diff options
Diffstat (limited to 'netwerk/dns/nsHostResolver.cpp')
-rw-r--r-- | netwerk/dns/nsHostResolver.cpp | 1911 |
1 files changed, 1911 insertions, 0 deletions
diff --git a/netwerk/dns/nsHostResolver.cpp b/netwerk/dns/nsHostResolver.cpp new file mode 100644 index 0000000000..ea4eaa6401 --- /dev/null +++ b/netwerk/dns/nsHostResolver.cpp @@ -0,0 +1,1911 @@ +/* vim:set ts=4 sw=2 sts=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#if defined(HAVE_RES_NINIT) +# include <sys/types.h> +# include <netinet/in.h> +# include <arpa/inet.h> +# include <arpa/nameser.h> +# include <resolv.h> +# define RES_RETRY_ON_FAILURE +#endif + +#include <stdlib.h> +#include <ctime> +#include "nsHostResolver.h" +#include "nsError.h" +#include "nsISupports.h" +#include "nsISupportsUtils.h" +#include "nsIThreadManager.h" +#include "nsComponentManagerUtils.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsXPCOMCIDInternal.h" +#include "prthread.h" +#include "prerror.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "PLDHashTable.h" +#include "nsQueryObject.h" +#include "nsURLHelper.h" +#include "nsThreadUtils.h" +#include "nsThreadPool.h" +#include "GetAddrInfo.h" +#include "TRR.h" +#include "TRRQuery.h" +#include "TRRService.h" + +#include "mozilla/Atomics.h" +#include "mozilla/HashFunctions.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_network.h" +// Put DNSLogging.h at the end to avoid LOG being overwritten by other headers. +#include "DNSLogging.h" + +#define IS_ADDR_TYPE(_type) ((_type) == nsIDNSService::RESOLVE_TYPE_DEFAULT) +#define IS_OTHER_TYPE(_type) ((_type) != nsIDNSService::RESOLVE_TYPE_DEFAULT) + +using namespace mozilla; +using namespace mozilla::net; + +// None of our implementations expose a TTL for negative responses, so we use a +// constant always. +static const unsigned int NEGATIVE_RECORD_LIFETIME = 60; + +//---------------------------------------------------------------------------- + +// Use a persistent thread pool in order to avoid spinning up new threads all +// the time. In particular, thread creation results in a res_init() call from +// libc which is quite expensive. +// +// The pool dynamically grows between 0 and MaxResolverThreads() in size. New +// requests go first to an idle thread. If that cannot be found and there are +// fewer than MaxResolverThreads() currently in the pool a new thread is created +// for high priority requests. If the new request is at a lower priority a new +// thread will only be created if there are fewer than +// MaxResolverThreadsAnyPriority() currently outstanding. If a thread cannot be +// created or an idle thread located for the request it is queued. +// +// When the pool is greater than MaxResolverThreadsAnyPriority() in size a +// thread will be destroyed after ShortIdleTimeoutSeconds of idle time. Smaller +// pools use LongIdleTimeoutSeconds for a timeout period. + +// for threads 1 -> MaxResolverThreadsAnyPriority() +#define LongIdleTimeoutSeconds 300 +// for threads MaxResolverThreadsAnyPriority() + 1 -> MaxResolverThreads() +#define ShortIdleTimeoutSeconds 60 + +//---------------------------------------------------------------------------- + +namespace mozilla::net { +LazyLogModule gHostResolverLog("nsHostResolver"); +} // namespace mozilla::net + +//---------------------------------------------------------------------------- + +#if defined(RES_RETRY_ON_FAILURE) + +// this class represents the resolver state for a given thread. if we +// encounter a lookup failure, then we can invoke the Reset method on an +// instance of this class to reset the resolver (in case /etc/resolv.conf +// for example changed). this is mainly an issue on GNU systems since glibc +// only reads in /etc/resolv.conf once per thread. it may be an issue on +// other systems as well. + +class nsResState { + public: + nsResState() + // initialize mLastReset to the time when this object + // is created. this means that a reset will not occur + // if a thread is too young. the alternative would be + // to initialize this to the beginning of time, so that + // the first failure would cause a reset, but since the + // thread would have just started up, it likely would + // already have current /etc/resolv.conf info. + : mLastReset(PR_IntervalNow()) {} + + bool Reset() { + // reset no more than once per second + if (PR_IntervalToSeconds(PR_IntervalNow() - mLastReset) < 1) { + return false; + } + + mLastReset = PR_IntervalNow(); + auto result = res_ninit(&_res); + + LOG(("nsResState::Reset() > 'res_ninit' returned %d", result)); + return (result == 0); + } + + private: + PRIntervalTime mLastReset; +}; + +#endif // RES_RETRY_ON_FAILURE + +//---------------------------------------------------------------------------- + +static const char kPrefGetTtl[] = "network.dns.get-ttl"; +static const char kPrefNativeIsLocalhost[] = "network.dns.native-is-localhost"; +static const char kPrefThreadIdleTime[] = + "network.dns.resolver-thread-extra-idle-time-seconds"; +static bool sGetTtlEnabled = false; +mozilla::Atomic<bool, mozilla::Relaxed> gNativeIsLocalhost; + +static void DnsPrefChanged(const char* aPref, void* aSelf) { + MOZ_ASSERT(NS_IsMainThread(), + "Should be getting pref changed notification on main thread!"); + + MOZ_ASSERT(aSelf); + + if (!strcmp(aPref, kPrefGetTtl)) { +#ifdef DNSQUERY_AVAILABLE + sGetTtlEnabled = Preferences::GetBool(kPrefGetTtl); +#endif + } else if (!strcmp(aPref, kPrefNativeIsLocalhost)) { + gNativeIsLocalhost = Preferences::GetBool(kPrefNativeIsLocalhost); + } +} + +NS_IMPL_ISUPPORTS0(nsHostResolver) + +nsHostResolver::nsHostResolver(uint32_t maxCacheEntries, + uint32_t defaultCacheEntryLifetime, + uint32_t defaultGracePeriod) + : mMaxCacheEntries(maxCacheEntries), + mDefaultCacheLifetime(defaultCacheEntryLifetime), + mDefaultGracePeriod(defaultGracePeriod), + mIdleTaskCV(mLock, "nsHostResolver.mIdleTaskCV") { + mCreationTime = PR_Now(); + + mLongIdleTimeout = TimeDuration::FromSeconds(LongIdleTimeoutSeconds); + mShortIdleTimeout = TimeDuration::FromSeconds(ShortIdleTimeoutSeconds); +} + +nsHostResolver::~nsHostResolver() = default; + +nsresult nsHostResolver::Init() MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_ASSERT(NS_IsMainThread()); + if (NS_FAILED(GetAddrInfoInit())) { + return NS_ERROR_FAILURE; + } + + LOG(("nsHostResolver::Init this=%p", this)); + + mShutdown = false; + mNCS = NetworkConnectivityService::GetSingleton(); + + // The preferences probably haven't been loaded from the disk yet, so we + // need to register a callback that will set up the experiment once they + // are. We also need to explicitly set a value for the props otherwise the + // callback won't be called. + { + DebugOnly<nsresult> rv = Preferences::RegisterCallbackAndCall( + &DnsPrefChanged, kPrefGetTtl, this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Could not register DNS TTL pref callback."); + rv = Preferences::RegisterCallbackAndCall(&DnsPrefChanged, + kPrefNativeIsLocalhost, this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Could not register DNS pref callback."); + } + +#if defined(HAVE_RES_NINIT) + // We want to make sure the system is using the correct resolver settings, + // so we force it to reload those settings whenever we startup a subsequent + // nsHostResolver instance. We assume that there is no reason to do this + // for the first nsHostResolver instance since that is usually created + // during application startup. + static int initCount = 0; + if (initCount++ > 0) { + auto result = res_ninit(&_res); + LOG(("nsHostResolver::Init > 'res_ninit' returned %d", result)); + } +#endif + + // We can configure the threadpool to keep threads alive for a while after + // the last ThreadFunc task has been executed. + int32_t poolTimeoutSecs = Preferences::GetInt(kPrefThreadIdleTime, 60); + uint32_t poolTimeoutMs; + if (poolTimeoutSecs < 0) { + // This means never shut down the idle threads + poolTimeoutMs = UINT32_MAX; + } else { + // We clamp down the idle time between 0 and one hour. + poolTimeoutMs = + mozilla::clamped<uint32_t>(poolTimeoutSecs * 1000, 0, 3600 * 1000); + } + + nsCOMPtr<nsIThreadPool> threadPool = new nsThreadPool(); + MOZ_ALWAYS_SUCCEEDS(threadPool->SetThreadLimit(MaxResolverThreads())); + MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadLimit(MaxResolverThreads())); + MOZ_ALWAYS_SUCCEEDS(threadPool->SetIdleThreadTimeout(poolTimeoutMs)); + MOZ_ALWAYS_SUCCEEDS( + threadPool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize)); + MOZ_ALWAYS_SUCCEEDS(threadPool->SetName("DNS Resolver"_ns)); + mResolverThreads = ToRefPtr(std::move(threadPool)); + + return NS_OK; +} + +void nsHostResolver::ClearPendingQueue( + LinkedList<RefPtr<nsHostRecord>>& aPendingQ) { + // loop through pending queue, erroring out pending lookups. + if (!aPendingQ.isEmpty()) { + for (const RefPtr<nsHostRecord>& rec : aPendingQ) { + rec->Cancel(); + if (rec->IsAddrRecord()) { + CompleteLookup(rec, NS_ERROR_ABORT, nullptr, rec->pb, rec->originSuffix, + rec->mTRRSkippedReason, nullptr); + } else { + mozilla::net::TypeRecordResultType empty(Nothing{}); + CompleteLookupByType(rec, NS_ERROR_ABORT, empty, 0, rec->pb); + } + } + } +} + +// +// FlushCache() is what we call when the network has changed. We must not +// trust names that were resolved before this change. They may resolve +// differently now. +// +// This function removes all existing resolved host entries from the hash. +// Names that are in the pending queues can be left there. Entries in the +// cache that have 'Resolve' set true but not 'OnQueue' are being resolved +// right now, so we need to mark them to get re-resolved on completion! + +void nsHostResolver::FlushCache(bool aTrrToo) { + MutexAutoLock lock(mLock); + + mQueue.FlushEvictionQ(mRecordDB, lock); + + // Refresh the cache entries that are resolving RIGHT now, remove the rest. + for (auto iter = mRecordDB.Iter(); !iter.Done(); iter.Next()) { + nsHostRecord* record = iter.UserData(); + // Try to remove the record, or mark it for refresh. + // By-type records are from TRR. We do not need to flush those entry + // when the network has change, because they are not local. + if (record->IsAddrRecord()) { + RefPtr<AddrHostRecord> addrRec = do_QueryObject(record); + MOZ_ASSERT(addrRec); + if (addrRec->RemoveOrRefresh(aTrrToo)) { + mQueue.MaybeRemoveFromQ(record, lock); + LOG(("Removing (%s) Addr record from mRecordDB", record->host.get())); + iter.Remove(); + } + } else if (aTrrToo) { + // remove by type records + LOG(("Removing (%s) type record from mRecordDB", record->host.get())); + iter.Remove(); + } + } +} + +void nsHostResolver::Shutdown() { + LOG(("Shutting down host resolver.\n")); + + { + DebugOnly<nsresult> rv = + Preferences::UnregisterCallback(&DnsPrefChanged, kPrefGetTtl, this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Could not unregister DNS TTL pref callback."); + } + + LinkedList<RefPtr<nsHostRecord>> pendingQHigh, pendingQMed, pendingQLow, + evictionQ; + + { + MutexAutoLock lock(mLock); + + mShutdown = true; + + if (mNumIdleTasks) { + mIdleTaskCV.NotifyAll(); + } + + mQueue.ClearAll( + [&](nsHostRecord* aRec) { + mLock.AssertCurrentThreadOwns(); + if (aRec->IsAddrRecord()) { + CompleteLookupLocked(aRec, NS_ERROR_ABORT, nullptr, aRec->pb, + aRec->originSuffix, aRec->mTRRSkippedReason, + nullptr, lock); + } else { + mozilla::net::TypeRecordResultType empty(Nothing{}); + CompleteLookupByTypeLocked(aRec, NS_ERROR_ABORT, empty, 0, aRec->pb, + lock); + } + }, + lock); + + for (const auto& data : mRecordDB.Values()) { + data->Cancel(); + } + // empty host database + mRecordDB.Clear(); + + mNCS = nullptr; + } + + // Shutdown the resolver threads, but with a timeout of 2 seconds (prefable). + // If the timeout is exceeded, any stuck threads will be leaked. + mResolverThreads->ShutdownWithTimeout( + StaticPrefs::network_dns_resolver_shutdown_timeout_ms()); + + { + mozilla::DebugOnly<nsresult> rv = GetAddrInfoShutdown(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to shutdown GetAddrInfo"); + } +} + +nsresult nsHostResolver::GetHostRecord( + const nsACString& host, const nsACString& aTrrServer, uint16_t type, + nsIDNSService::DNSFlags flags, uint16_t af, bool pb, + const nsCString& originSuffix, nsHostRecord** result) { + MutexAutoLock lock(mLock); + nsHostKey key(host, aTrrServer, type, flags, af, pb, originSuffix); + + RefPtr<nsHostRecord> rec = + mRecordDB.LookupOrInsertWith(key, [&] { return InitRecord(key); }); + if (rec->IsAddrRecord()) { + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + if (addrRec->addr) { + return NS_ERROR_FAILURE; + } + } + + if (rec->mResolving) { + return NS_ERROR_FAILURE; + } + + *result = rec.forget().take(); + return NS_OK; +} + +nsHostRecord* nsHostResolver::InitRecord(const nsHostKey& key) { + if (IS_ADDR_TYPE(key.type)) { + return new AddrHostRecord(key); + } + return new TypeHostRecord(key); +} + +already_AddRefed<nsHostRecord> nsHostResolver::InitLoopbackRecord( + const nsHostKey& key, nsresult* aRv) { + MOZ_ASSERT(aRv); + MOZ_ASSERT(IS_ADDR_TYPE(key.type)); + + *aRv = NS_ERROR_FAILURE; + RefPtr<nsHostRecord> rec = InitRecord(key); + + nsTArray<NetAddr> addresses; + NetAddr addr; + if (key.af == PR_AF_INET || key.af == PR_AF_UNSPEC) { + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(addr.InitFromString("127.0.0.1"_ns))); + addresses.AppendElement(addr); + } + if (key.af == PR_AF_INET6 || key.af == PR_AF_UNSPEC) { + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(addr.InitFromString("::1"_ns))); + addresses.AppendElement(addr); + } + + RefPtr<AddrInfo> ai = + new AddrInfo(rec->host, DNSResolverType::Native, 0, std::move(addresses)); + + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + MutexAutoLock lock(addrRec->addr_info_lock); + addrRec->addr_info = ai; + addrRec->SetExpiration(TimeStamp::NowLoRes(), mDefaultCacheLifetime, + mDefaultGracePeriod); + addrRec->negative = false; + + *aRv = NS_OK; + return rec.forget(); +} + +nsresult nsHostResolver::ResolveHost(const nsACString& aHost, + const nsACString& aTrrServer, + int32_t aPort, uint16_t type, + const OriginAttributes& aOriginAttributes, + nsIDNSService::DNSFlags flags, uint16_t af, + nsResolveHostCallback* aCallback) { + nsAutoCString host(aHost); + NS_ENSURE_TRUE(!host.IsEmpty(), NS_ERROR_UNEXPECTED); + + nsAutoCString originSuffix; + aOriginAttributes.CreateSuffix(originSuffix); + LOG(("Resolving host [%s]<%s>%s%s type %d. [this=%p]\n", host.get(), + originSuffix.get(), flags & RES_BYPASS_CACHE ? " - bypassing cache" : "", + flags & RES_REFRESH_CACHE ? " - refresh cache" : "", type, this)); + + // ensure that we are working with a valid hostname before proceeding. see + // bug 304904 for details. + if (!net_IsValidHostName(host)) { + return NS_ERROR_UNKNOWN_HOST; + } + + // By-Type requests use only TRR. If TRR is disabled we can return + // immediately. + if (IS_OTHER_TYPE(type) && Mode() == nsIDNSService::MODE_TRROFF) { + return NS_ERROR_UNKNOWN_HOST; + } + + // Used to try to parse to an IP address literal. + NetAddr tempAddr; + if (IS_OTHER_TYPE(type) && (NS_SUCCEEDED(tempAddr.InitFromString(host)))) { + // For by-type queries the host cannot be IP literal. + return NS_ERROR_UNKNOWN_HOST; + } + + RefPtr<nsResolveHostCallback> callback(aCallback); + // if result is set inside the lock, then we need to issue the + // callback before returning. + RefPtr<nsHostRecord> result; + nsresult status = NS_OK, rv = NS_OK; + { + MutexAutoLock lock(mLock); + + if (mShutdown) { + return NS_ERROR_NOT_INITIALIZED; + } + + // check to see if there is already an entry for this |host| + // in the hash table. if so, then check to see if we can't + // just reuse the lookup result. otherwise, if there are + // any pending callbacks, then add to pending callbacks queue, + // and return. otherwise, add ourselves as first pending + // callback, and proceed to do the lookup. + + Maybe<nsCString> originHost; + if (StaticPrefs::network_dns_port_prefixed_qname_https_rr() && + type == nsIDNSService::RESOLVE_TYPE_HTTPSSVC && aPort != -1 && + aPort != 443) { + originHost = Some(host); + host = nsPrintfCString("_%d._https.%s", aPort, host.get()); + LOG((" Using port prefixed host name [%s]", host.get())); + } + + bool excludedFromTRR = false; + if (TRRService::Get() && TRRService::Get()->IsExcludedFromTRR(host)) { + flags |= nsIDNSService::RESOLVE_DISABLE_TRR; + excludedFromTRR = true; + + if (!aTrrServer.IsEmpty()) { + return NS_ERROR_UNKNOWN_HOST; + } + } + + nsHostKey key(host, aTrrServer, type, flags, af, + (aOriginAttributes.mPrivateBrowsingId > 0), originSuffix); + + // Check if we have a localhost domain, if so hardcode to loopback + if (IS_ADDR_TYPE(type) && IsLoopbackHostname(host)) { + nsresult rv; + RefPtr<nsHostRecord> result = InitLoopbackRecord(key, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + MOZ_ASSERT(result); + aCallback->OnResolveHostComplete(this, result, NS_OK); + return NS_OK; + } + + RefPtr<nsHostRecord> rec = + mRecordDB.LookupOrInsertWith(key, [&] { return InitRecord(key); }); + + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + MOZ_ASSERT(rec, "Record should not be null"); + MOZ_ASSERT((IS_ADDR_TYPE(type) && rec->IsAddrRecord() && addrRec) || + (IS_OTHER_TYPE(type) && !rec->IsAddrRecord())); + + if (IS_OTHER_TYPE(type) && originHost) { + RefPtr<TypeHostRecord> typeRec = do_QueryObject(rec); + typeRec->mOriginHost = std::move(originHost); + } + + if (excludedFromTRR) { + rec->RecordReason(TRRSkippedReason::TRR_EXCLUDED); + } + + if (!(flags & RES_BYPASS_CACHE) && + rec->HasUsableResult(TimeStamp::NowLoRes(), flags)) { + result = FromCache(rec, host, type, status, lock); + } else if (addrRec && addrRec->addr) { + // if the host name is an IP address literal and has been + // parsed, go ahead and use it. + LOG((" Using cached address for IP Literal [%s].\n", host.get())); + result = FromCachedIPLiteral(rec); + } else if (addrRec && NS_SUCCEEDED(tempAddr.InitFromString(host))) { + // try parsing the host name as an IP address literal to short + // circuit full host resolution. (this is necessary on some + // platforms like Win9x. see bug 219376 for more details.) + LOG((" Host is IP Literal [%s].\n", host.get())); + result = FromIPLiteral(addrRec, tempAddr); + } else if (mQueue.PendingCount() >= MAX_NON_PRIORITY_REQUESTS && + !IsHighPriority(flags) && !rec->mResolving) { + LOG( + (" Lookup queue full: dropping %s priority request for " + "host [%s].\n", + IsMediumPriority(flags) ? "medium" : "low", host.get())); + if (IS_ADDR_TYPE(type)) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_OVERFLOW); + } + // This is a lower priority request and we are swamped, so refuse it. + rv = NS_ERROR_DNS_LOOKUP_QUEUE_FULL; + + // Check if the offline flag is set. + } else if (flags & RES_OFFLINE) { + LOG((" Offline request for host [%s]; ignoring.\n", host.get())); + rv = NS_ERROR_OFFLINE; + + // We do not have a valid result till here. + // A/AAAA request can check for an alternative entry like AF_UNSPEC. + // Otherwise we need to start a new query. + } else if (!rec->mResolving) { + result = + FromUnspecEntry(rec, host, aTrrServer, originSuffix, type, flags, af, + aOriginAttributes.mPrivateBrowsingId > 0, status); + // If this is a by-type request or if no valid record was found + // in the cache or this is an AF_UNSPEC request, then start a + // new lookup. + if (!result) { + LOG((" No usable record in cache for host [%s] type %d.", host.get(), + type)); + + if (flags & RES_REFRESH_CACHE) { + rec->Invalidate(); + } + + // Add callback to the list of pending callbacks. + rec->mCallbacks.insertBack(callback); + rec->flags = flags; + rv = NameLookup(rec, lock); + if (IS_ADDR_TYPE(type)) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, + METHOD_NETWORK_FIRST); + } + if (NS_FAILED(rv) && callback->isInList()) { + callback->remove(); + } else { + LOG( + (" DNS lookup for host [%s] blocking " + "pending 'getaddrinfo' or trr query: " + "callback [%p]", + host.get(), callback.get())); + } + } + } else { + LOG( + (" Host [%s] is being resolved. Appending callback " + "[%p].", + host.get(), callback.get())); + + rec->mCallbacks.insertBack(callback); + + // Only A/AAAA records are place in a queue. The queues are for + // the native resolver, therefore by-type request are never put + // into a queue. + if (addrRec && addrRec->onQueue()) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, + METHOD_NETWORK_SHARED); + + // Consider the case where we are on a pending queue of + // lower priority than the request is being made at. + // In that case we should upgrade to the higher queue. + + if (IsHighPriority(flags) && !IsHighPriority(rec->flags)) { + // Move from (low|med) to high. + mQueue.MoveToAnotherPendingQ(rec, flags, lock); + rec->flags = flags; + ConditionallyCreateThread(rec); + } else if (IsMediumPriority(flags) && IsLowPriority(rec->flags)) { + // Move from low to med. + mQueue.MoveToAnotherPendingQ(rec, flags, lock); + rec->flags = flags; + mIdleTaskCV.Notify(); + } + } + } + + if (result && callback->isInList()) { + callback->remove(); + } + } // lock + + if (result) { + callback->OnResolveHostComplete(this, result, status); + } + + return rv; +} + +already_AddRefed<nsHostRecord> nsHostResolver::FromCache( + nsHostRecord* aRec, const nsACString& aHost, uint16_t aType, + nsresult& aStatus, const MutexAutoLock& aLock) { + LOG((" Using cached record for host [%s].\n", + nsPromiseFlatCString(aHost).get())); + + // put reference to host record on stack... + RefPtr<nsHostRecord> result = aRec; + if (IS_ADDR_TYPE(aType)) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_HIT); + } + + // For entries that are in the grace period + // or all cached negative entries, use the cache but start a new + // lookup in the background + ConditionallyRefreshRecord(aRec, aHost, aLock); + + if (aRec->negative) { + LOG((" Negative cache entry for host [%s].\n", + nsPromiseFlatCString(aHost).get())); + if (IS_ADDR_TYPE(aType)) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_NEGATIVE_HIT); + } + aStatus = NS_ERROR_UNKNOWN_HOST; + } + + return result.forget(); +} + +already_AddRefed<nsHostRecord> nsHostResolver::FromCachedIPLiteral( + nsHostRecord* aRec) { + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_LITERAL); + RefPtr<nsHostRecord> result = aRec; + return result.forget(); +} + +already_AddRefed<nsHostRecord> nsHostResolver::FromIPLiteral( + AddrHostRecord* aAddrRec, const NetAddr& aAddr) { + // ok, just copy the result into the host record, and be + // done with it! ;-) + aAddrRec->addr = MakeUnique<NetAddr>(aAddr); + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_LITERAL); + // put reference to host record on stack... + RefPtr<nsHostRecord> result = aAddrRec; + return result.forget(); +} + +already_AddRefed<nsHostRecord> nsHostResolver::FromUnspecEntry( + nsHostRecord* aRec, const nsACString& aHost, const nsACString& aTrrServer, + const nsACString& aOriginSuffix, uint16_t aType, + nsIDNSService::DNSFlags aFlags, uint16_t af, bool aPb, nsresult& aStatus) { + RefPtr<nsHostRecord> result = nullptr; + // If this is an IPV4 or IPV6 specific request, check if there is + // an AF_UNSPEC entry we can use. Otherwise, hit the resolver... + RefPtr<AddrHostRecord> addrRec = do_QueryObject(aRec); + if (addrRec && !(aFlags & RES_BYPASS_CACHE) && + ((af == PR_AF_INET) || (af == PR_AF_INET6))) { + // Check for an AF_UNSPEC entry. + + const nsHostKey unspecKey(aHost, aTrrServer, + nsIDNSService::RESOLVE_TYPE_DEFAULT, aFlags, + PR_AF_UNSPEC, aPb, aOriginSuffix); + RefPtr<nsHostRecord> unspecRec = mRecordDB.Get(unspecKey); + + TimeStamp now = TimeStamp::NowLoRes(); + if (unspecRec && unspecRec->HasUsableResult(now, aFlags)) { + MOZ_ASSERT(unspecRec->IsAddrRecord()); + + RefPtr<AddrHostRecord> addrUnspecRec = do_QueryObject(unspecRec); + MOZ_ASSERT(addrUnspecRec); + MOZ_ASSERT(addrUnspecRec->addr_info || addrUnspecRec->negative, + "Entry should be resolved or negative."); + + LOG((" Trying AF_UNSPEC entry for host [%s] af: %s.\n", + PromiseFlatCString(aHost).get(), + (af == PR_AF_INET) ? "AF_INET" : "AF_INET6")); + + // We need to lock in case any other thread is reading + // addr_info. + MutexAutoLock lock(addrRec->addr_info_lock); + + addrRec->addr_info = nullptr; + addrRec->addr_info_gencnt++; + if (unspecRec->negative) { + aRec->negative = unspecRec->negative; + aRec->CopyExpirationTimesAndFlagsFrom(unspecRec); + } else if (addrUnspecRec->addr_info) { + MutexAutoLock lock(addrUnspecRec->addr_info_lock); + if (addrUnspecRec->addr_info) { + // Search for any valid address in the AF_UNSPEC entry + // in the cache (not blocklisted and from the right + // family). + nsTArray<NetAddr> addresses; + for (const auto& addr : addrUnspecRec->addr_info->Addresses()) { + if ((af == addr.inet.family) && + !addrUnspecRec->Blocklisted(&addr)) { + addresses.AppendElement(addr); + } + } + if (!addresses.IsEmpty()) { + addrRec->addr_info = new AddrInfo( + addrUnspecRec->addr_info->Hostname(), + addrUnspecRec->addr_info->CanonicalHostname(), + addrUnspecRec->addr_info->ResolverType(), + addrUnspecRec->addr_info->TRRType(), std::move(addresses)); + addrRec->addr_info_gencnt++; + aRec->CopyExpirationTimesAndFlagsFrom(unspecRec); + } + } + } + // Now check if we have a new record. + if (aRec->HasUsableResult(now, aFlags)) { + result = aRec; + if (aRec->negative) { + aStatus = NS_ERROR_UNKNOWN_HOST; + } + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_HIT); + ConditionallyRefreshRecord(aRec, aHost, lock); + } else if (af == PR_AF_INET6) { + // For AF_INET6, a new lookup means another AF_UNSPEC + // lookup. We have already iterated through the + // AF_UNSPEC addresses, so we mark this record as + // negative. + LOG( + (" No AF_INET6 in AF_UNSPEC entry: " + "host [%s] unknown host.", + nsPromiseFlatCString(aHost).get())); + result = aRec; + aRec->negative = true; + aStatus = NS_ERROR_UNKNOWN_HOST; + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, + METHOD_NEGATIVE_HIT); + } + } + } + + return result.forget(); +} + +void nsHostResolver::DetachCallback( + const nsACString& host, const nsACString& aTrrServer, uint16_t aType, + const OriginAttributes& aOriginAttributes, nsIDNSService::DNSFlags flags, + uint16_t af, nsResolveHostCallback* aCallback, nsresult status) { + RefPtr<nsHostRecord> rec; + RefPtr<nsResolveHostCallback> callback(aCallback); + + { + MutexAutoLock lock(mLock); + + nsAutoCString originSuffix; + aOriginAttributes.CreateSuffix(originSuffix); + + nsHostKey key(host, aTrrServer, aType, flags, af, + (aOriginAttributes.mPrivateBrowsingId > 0), originSuffix); + RefPtr<nsHostRecord> entry = mRecordDB.Get(key); + if (entry) { + // walk list looking for |callback|... we cannot assume + // that it will be there! + + for (nsResolveHostCallback* c : entry->mCallbacks) { + if (c == callback) { + rec = entry; + c->remove(); + break; + } + } + } + } + + // complete callback with the given status code; this would only be done if + // the record was in the process of being resolved. + if (rec) { + callback->OnResolveHostComplete(this, rec, status); + } +} + +nsresult nsHostResolver::ConditionallyCreateThread(nsHostRecord* rec) { + if (mNumIdleTasks) { + // wake up idle tasks to process this lookup + mIdleTaskCV.Notify(); + } else if ((mActiveTaskCount < MaxResolverThreadsAnyPriority()) || + (IsHighPriority(rec->flags) && + mActiveTaskCount < MaxResolverThreads())) { + nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod( + "nsHostResolver::ThreadFunc", this, &nsHostResolver::ThreadFunc); + mActiveTaskCount++; + nsresult rv = + mResolverThreads->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + mActiveTaskCount--; + } + } else { + LOG((" Unable to find a thread for looking up host [%s].\n", + rec->host.get())); + } + return NS_OK; +} + +nsresult nsHostResolver::TrrLookup_unlocked(nsHostRecord* rec, TRR* pushedTRR) { + MutexAutoLock lock(mLock); + return TrrLookup(rec, lock, pushedTRR); +} + +void nsHostResolver::MaybeRenewHostRecord(nsHostRecord* aRec) { + MutexAutoLock lock(mLock); + MaybeRenewHostRecordLocked(aRec, lock); +} + +void nsHostResolver::MaybeRenewHostRecordLocked(nsHostRecord* aRec, + const MutexAutoLock& aLock) { + mQueue.MaybeRenewHostRecord(aRec, aLock); +} + +bool nsHostResolver::TRRServiceEnabledForRecord(nsHostRecord* aRec) { + MOZ_ASSERT(aRec, "Record must not be empty"); + MOZ_ASSERT(aRec->mEffectiveTRRMode != nsIRequest::TRR_DEFAULT_MODE, + "effective TRR mode must be computed before this call"); + if (!TRRService::Get()) { + aRec->RecordReason(TRRSkippedReason::TRR_NO_GSERVICE); + return false; + } + + // We always try custom resolvers. + if (!aRec->mTrrServer.IsEmpty()) { + return true; + } + + nsIRequest::TRRMode reqMode = aRec->mEffectiveTRRMode; + if (TRRService::Get()->Enabled(reqMode)) { + return true; + } + + if (NS_IsOffline()) { + // If we are in the NOT_CONFIRMED state _because_ we lack connectivity, + // then we should report that the browser is offline instead. + aRec->RecordReason(TRRSkippedReason::TRR_IS_OFFLINE); + return false; + } + + auto hasConnectivity = [this]() -> bool { + mLock.AssertCurrentThreadOwns(); + if (!mNCS) { + return true; + } + nsINetworkConnectivityService::ConnectivityState ipv4 = mNCS->GetIPv4(); + nsINetworkConnectivityService::ConnectivityState ipv6 = mNCS->GetIPv6(); + + if (ipv4 == nsINetworkConnectivityService::OK || + ipv6 == nsINetworkConnectivityService::OK) { + return true; + } + + if (ipv4 == nsINetworkConnectivityService::UNKNOWN || + ipv6 == nsINetworkConnectivityService::UNKNOWN) { + // One of the checks hasn't completed yet. Optimistically assume we'll + // have network connectivity. + return true; + } + + return false; + }; + + if (!hasConnectivity()) { + aRec->RecordReason(TRRSkippedReason::TRR_NO_CONNECTIVITY); + return false; + } + + bool isConfirmed = TRRService::Get()->IsConfirmed(); + if (!isConfirmed) { + aRec->RecordReason(TRRSkippedReason::TRR_NOT_CONFIRMED); + } + + return isConfirmed; +} + +// returns error if no TRR resolve is issued +// it is impt this is not called while a native lookup is going on +nsresult nsHostResolver::TrrLookup(nsHostRecord* aRec, + const MutexAutoLock& aLock, TRR* pushedTRR) { + if (Mode() == nsIDNSService::MODE_TRROFF || + StaticPrefs::network_dns_disabled()) { + return NS_ERROR_UNKNOWN_HOST; + } + LOG(("TrrLookup host:%s af:%" PRId16, aRec->host.get(), aRec->af)); + + RefPtr<nsHostRecord> rec(aRec); + mLock.AssertCurrentThreadOwns(); + + RefPtr<AddrHostRecord> addrRec; + RefPtr<TypeHostRecord> typeRec; + + if (rec->IsAddrRecord()) { + addrRec = do_QueryObject(rec); + MOZ_ASSERT(addrRec); + } else { + typeRec = do_QueryObject(rec); + MOZ_ASSERT(typeRec); + } + + MOZ_ASSERT(!rec->mResolving); + + if (!TRRServiceEnabledForRecord(aRec)) { + return NS_ERROR_UNKNOWN_HOST; + } + + MaybeRenewHostRecordLocked(rec, aLock); + + RefPtr<TRRQuery> query = new TRRQuery(this, rec); + nsresult rv = query->DispatchLookup(pushedTRR); + if (NS_FAILED(rv)) { + rec->RecordReason(TRRSkippedReason::TRR_DID_NOT_MAKE_QUERY); + return rv; + } + + { + auto lock = rec->mTRRQuery.Lock(); + MOZ_ASSERT(!lock.ref(), "TRR already in progress"); + lock.ref() = query; + } + + rec->mResolving++; + rec->mTrrAttempts++; + return NS_OK; +} + +nsresult nsHostResolver::NativeLookup(nsHostRecord* aRec, + const MutexAutoLock& aLock) { + if (StaticPrefs::network_dns_disabled()) { + return NS_ERROR_UNKNOWN_HOST; + } + LOG(("NativeLookup host:%s af:%" PRId16, aRec->host.get(), aRec->af)); + + // Only A/AAAA request are resolve natively. + MOZ_ASSERT(aRec->IsAddrRecord()); + mLock.AssertCurrentThreadOwns(); + + RefPtr<nsHostRecord> rec(aRec); + RefPtr<AddrHostRecord> addrRec; + addrRec = do_QueryObject(rec); + MOZ_ASSERT(addrRec); + + addrRec->mNativeStart = TimeStamp::Now(); + + // Add rec to one of the pending queues, possibly removing it from mEvictionQ. + MaybeRenewHostRecordLocked(aRec, aLock); + + mQueue.InsertRecord(rec, rec->flags, aLock); + + addrRec->StoreNative(true); + addrRec->StoreNativeUsed(true); + addrRec->mResolving++; + + nsresult rv = ConditionallyCreateThread(rec); + + LOG((" DNS thread counters: total=%d any-live=%d idle=%d pending=%d\n", + static_cast<uint32_t>(mActiveTaskCount), + static_cast<uint32_t>(mActiveAnyThreadCount), + static_cast<uint32_t>(mNumIdleTasks), mQueue.PendingCount())); + + return rv; +} + +// static +nsIDNSService::ResolverMode nsHostResolver::Mode() { + if (TRRService::Get()) { + return TRRService::Get()->Mode(); + } + + // If we don't have a TRR service just return MODE_TRROFF so we don't make + // any TRR requests by mistake. + return nsIDNSService::MODE_TRROFF; +} + +nsIRequest::TRRMode nsHostRecord::TRRMode() { + return nsIDNSService::GetTRRModeFromFlags(flags); +} + +// static +void nsHostResolver::ComputeEffectiveTRRMode(nsHostRecord* aRec) { + nsIDNSService::ResolverMode resolverMode = nsHostResolver::Mode(); + nsIRequest::TRRMode requestMode = aRec->TRRMode(); + + // For domains that are excluded from TRR or when parental control is enabled, + // we fallback to NativeLookup. This happens even in MODE_TRRONLY. By default + // localhost and local are excluded (so we cover *.local hosts) See the + // network.trr.excluded-domains pref. + + if (!TRRService::Get()) { + aRec->RecordReason(TRRSkippedReason::TRR_NO_GSERVICE); + aRec->mEffectiveTRRMode = requestMode; + return; + } + + if (!aRec->mTrrServer.IsEmpty()) { + aRec->mEffectiveTRRMode = nsIRequest::TRR_ONLY_MODE; + return; + } + + if (TRRService::Get()->IsExcludedFromTRR(aRec->host)) { + aRec->RecordReason(TRRSkippedReason::TRR_EXCLUDED); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + + if (StaticPrefs::network_dns_skipTRR_when_parental_control_enabled() && + TRRService::Get()->ParentalControlEnabled()) { + aRec->RecordReason(TRRSkippedReason::TRR_PARENTAL_CONTROL); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + + if (resolverMode == nsIDNSService::MODE_TRROFF) { + aRec->RecordReason(TRRSkippedReason::TRR_OFF_EXPLICIT); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + + if (requestMode == nsIRequest::TRR_DISABLED_MODE) { + aRec->RecordReason(TRRSkippedReason::TRR_REQ_MODE_DISABLED); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + + if ((requestMode == nsIRequest::TRR_DEFAULT_MODE && + resolverMode == nsIDNSService::MODE_NATIVEONLY)) { + if (StaticPrefs::network_trr_display_fallback_warning()) { + TRRSkippedReason heuristicResult = + TRRService::Get()->GetHeuristicDetectionResult(); + if (heuristicResult != TRRSkippedReason::TRR_UNSET && + heuristicResult != TRRSkippedReason::TRR_OK) { + aRec->RecordReason(heuristicResult); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + } + aRec->RecordReason(TRRSkippedReason::TRR_MODE_NOT_ENABLED); + aRec->mEffectiveTRRMode = nsIRequest::TRR_DISABLED_MODE; + return; + } + + if (requestMode == nsIRequest::TRR_DEFAULT_MODE && + resolverMode == nsIDNSService::MODE_TRRFIRST) { + aRec->mEffectiveTRRMode = nsIRequest::TRR_FIRST_MODE; + return; + } + + if (requestMode == nsIRequest::TRR_DEFAULT_MODE && + resolverMode == nsIDNSService::MODE_TRRONLY) { + aRec->mEffectiveTRRMode = nsIRequest::TRR_ONLY_MODE; + return; + } + + aRec->mEffectiveTRRMode = requestMode; +} + +// Kick-off a name resolve operation, using native resolver and/or TRR +nsresult nsHostResolver::NameLookup(nsHostRecord* rec, + const mozilla::MutexAutoLock& aLock) { + LOG(("NameLookup host:%s af:%" PRId16, rec->host.get(), rec->af)); + mLock.AssertCurrentThreadOwns(); + + if (rec->flags & RES_IP_HINT) { + LOG(("Skip lookup if RES_IP_HINT is set\n")); + return NS_ERROR_UNKNOWN_HOST; + } + + nsresult rv = NS_ERROR_UNKNOWN_HOST; + if (rec->mResolving) { + LOG(("NameLookup %s while already resolving\n", rec->host.get())); + return NS_OK; + } + + // Make sure we reset the reason each time we attempt to do a new lookup + // so we don't wrongly report the reason for the previous one. + rec->Reset(); + + ComputeEffectiveTRRMode(rec); + + if (!rec->mTrrServer.IsEmpty()) { + LOG(("NameLookup: %s use trr:%s", rec->host.get(), rec->mTrrServer.get())); + if (rec->mEffectiveTRRMode != nsIRequest::TRR_ONLY_MODE) { + return NS_ERROR_UNKNOWN_HOST; + } + + if (rec->flags & nsIDNSService::RESOLVE_DISABLE_TRR) { + LOG(("TRR with server and DISABLE_TRR flag. Returning error.")); + return NS_ERROR_UNKNOWN_HOST; + } + return TrrLookup(rec, aLock); + } + + LOG(("NameLookup: %s effectiveTRRmode: %d flags: %X", rec->host.get(), + static_cast<nsIRequest::TRRMode>(rec->mEffectiveTRRMode), rec->flags)); + + if (rec->flags & nsIDNSService::RESOLVE_DISABLE_TRR) { + rec->RecordReason(TRRSkippedReason::TRR_DISABLED_FLAG); + } + + bool serviceNotReady = !TRRServiceEnabledForRecord(rec); + + if (rec->mEffectiveTRRMode != nsIRequest::TRR_DISABLED_MODE && + !((rec->flags & nsIDNSService::RESOLVE_DISABLE_TRR)) && + !serviceNotReady) { + rv = TrrLookup(rec, aLock); + } + + if (rec->mEffectiveTRRMode == nsIRequest::TRR_DISABLED_MODE || + (rec->mEffectiveTRRMode == nsIRequest::TRR_FIRST_MODE && + (rec->flags & nsIDNSService::RESOLVE_DISABLE_TRR || serviceNotReady || + NS_FAILED(rv)))) { + if (!rec->IsAddrRecord()) { + return rv; + } + +#ifdef DEBUG + // If we use this branch then the mTRRUsed flag should not be set + // Even if we did call TrrLookup above, the fact that it failed sync-ly + // means that we didn't actually succeed in opening the channel. + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + MOZ_ASSERT(addrRec && addrRec->mResolverType == DNSResolverType::Native); +#endif + + // We did not lookup via TRR - don't fallback to native if the + // network.trr.display_fallback_warning pref is set and either + // 1. we are in TRR first mode and confirmation failed + // 2. the record has trr_disabled and a heuristic skip reason + if (StaticPrefs::network_trr_display_fallback_warning() && + rec->mEffectiveTRRMode != nsIRequest::TRR_ONLY_MODE) { + if ((rec->mEffectiveTRRMode == nsIRequest::TRR_FIRST_MODE && + rec->mTRRSkippedReason == TRRSkippedReason::TRR_NOT_CONFIRMED) || + (rec->mEffectiveTRRMode == nsIRequest::TRR_DISABLED_MODE && + rec->mTRRSkippedReason >= + nsITRRSkipReason::TRR_HEURISTIC_TRIPPED_GOOGLE_SAFESEARCH && + rec->mTRRSkippedReason <= + nsITRRSkipReason::TRR_HEURISTIC_TRIPPED_NRPT)) { + LOG(( + "NameLookup: ResolveHostComplete with status NS_ERROR_UNKNOWN_HOST " + "for: %s effectiveTRRmode: " + "%d SkippedReason: %d", + rec->host.get(), + static_cast<nsIRequest::TRRMode>(rec->mEffectiveTRRMode), + static_cast<int32_t>(rec->mTRRSkippedReason))); + + mozilla::LinkedList<RefPtr<nsResolveHostCallback>> cbs = + std::move(rec->mCallbacks); + for (nsResolveHostCallback* c = cbs.getFirst(); c; + c = c->removeAndGetNext()) { + c->OnResolveHostComplete(this, rec, NS_ERROR_UNKNOWN_HOST); + } + + return NS_OK; + } + } + + rv = NativeLookup(rec, aLock); + } + + return rv; +} + +nsresult nsHostResolver::ConditionallyRefreshRecord( + nsHostRecord* rec, const nsACString& host, const MutexAutoLock& aLock) { + if ((rec->CheckExpiration(TimeStamp::NowLoRes()) != nsHostRecord::EXP_VALID || + rec->negative) && + !rec->mResolving && rec->RefreshForNegativeResponse()) { + LOG((" Using %s cache entry for host [%s] but starting async renewal.", + rec->negative ? "negative" : "positive", host.BeginReading())); + NameLookup(rec, aLock); + + if (rec->IsAddrRecord() && !rec->negative) { + // negative entries are constantly being refreshed, only + // track positive grace period induced renewals + Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_RENEWAL); + } + } + return NS_OK; +} + +bool nsHostResolver::GetHostToLookup(AddrHostRecord** result) { + bool timedOut = false; + TimeDuration timeout; + TimeStamp epoch, now; + + MutexAutoLock lock(mLock); + + timeout = (mNumIdleTasks >= MaxResolverThreadsAnyPriority()) + ? mShortIdleTimeout + : mLongIdleTimeout; + epoch = TimeStamp::Now(); + + while (!mShutdown) { + // remove next record from Q; hand over owning reference. Check high, then + // med, then low + +#define SET_GET_TTL(var, val) (var)->StoreGetTtl(sGetTtlEnabled && (val)) + + RefPtr<AddrHostRecord> addrRec = mQueue.Dequeue(true, lock); + if (addrRec) { + SET_GET_TTL(addrRec, false); + addrRec.forget(result); + return true; + } + + if (mActiveAnyThreadCount < MaxResolverThreadsAnyPriority()) { + addrRec = mQueue.Dequeue(false, lock); + if (addrRec) { + MOZ_ASSERT(IsMediumPriority(addrRec->flags) || + IsLowPriority(addrRec->flags)); + mActiveAnyThreadCount++; + addrRec->StoreUsingAnyThread(true); + SET_GET_TTL(addrRec, true); + addrRec.forget(result); + return true; + } + } + + // Determining timeout is racy, so allow one cycle through checking the + // queues before exiting. + if (timedOut) { + break; + } + + // wait for one or more of the following to occur: + // (1) the pending queue has a host record to process + // (2) the shutdown flag has been set + // (3) the thread has been idle for too long + + mNumIdleTasks++; + mIdleTaskCV.Wait(timeout); + mNumIdleTasks--; + + now = TimeStamp::Now(); + + if (now - epoch >= timeout) { + timedOut = true; + } else { + // It is possible that CondVar::Wait() was interrupted and returned + // early, in which case we will loop back and re-enter it. In that + // case we want to do so with the new timeout reduced to reflect + // time already spent waiting. + timeout -= now - epoch; + epoch = now; + } + } + + // tell thread to exit... + return false; +} + +void nsHostResolver::PrepareRecordExpirationAddrRecord( + AddrHostRecord* rec) const { + // NOTE: rec->addr_info_lock is already held by parent + MOZ_ASSERT(((bool)rec->addr_info) != rec->negative); + mLock.AssertCurrentThreadOwns(); + if (!rec->addr_info) { + rec->SetExpiration(TimeStamp::NowLoRes(), NEGATIVE_RECORD_LIFETIME, 0); + LOG(("Caching host [%s] negative record for %u seconds.\n", rec->host.get(), + NEGATIVE_RECORD_LIFETIME)); + return; + } + + unsigned int lifetime = mDefaultCacheLifetime; + unsigned int grace = mDefaultGracePeriod; + + unsigned int ttl = mDefaultCacheLifetime; + if (sGetTtlEnabled || rec->addr_info->IsTRR()) { + if (rec->addr_info && rec->addr_info->TTL() != AddrInfo::NO_TTL_DATA) { + ttl = rec->addr_info->TTL(); + } + lifetime = ttl; + grace = 0; + } + + rec->SetExpiration(TimeStamp::NowLoRes(), lifetime, grace); + LOG(("Caching host [%s] record for %u seconds (grace %d).", rec->host.get(), + lifetime, grace)); +} + +static bool different_rrset(AddrInfo* rrset1, AddrInfo* rrset2) { + if (!rrset1 || !rrset2) { + return true; + } + + LOG(("different_rrset %s\n", rrset1->Hostname().get())); + + if (rrset1->ResolverType() != rrset2->ResolverType()) { + return true; + } + + if (rrset1->TRRType() != rrset2->TRRType()) { + return true; + } + + if (rrset1->Addresses().Length() != rrset2->Addresses().Length()) { + LOG(("different_rrset true due to length change\n")); + return true; + } + + nsTArray<NetAddr> orderedSet1 = rrset1->Addresses().Clone(); + nsTArray<NetAddr> orderedSet2 = rrset2->Addresses().Clone(); + orderedSet1.Sort(); + orderedSet2.Sort(); + + bool eq = orderedSet1 == orderedSet2; + if (!eq) { + LOG(("different_rrset true due to content change\n")); + } else { + LOG(("different_rrset false\n")); + } + return !eq; +} + +void nsHostResolver::AddToEvictionQ(nsHostRecord* rec, + const MutexAutoLock& aLock) { + mQueue.AddToEvictionQ(rec, mMaxCacheEntries, mRecordDB, aLock); +} + +// After a first lookup attempt with TRR in mode 2, we may: +// - If network.trr.retry_on_recoverable_errors is false, retry with native. +// - If network.trr.retry_on_recoverable_errors is true: +// - Retry with native if the first attempt failed because we got NXDOMAIN, an +// unreachable address (TRR_DISABLED_FLAG), or we skipped TRR because +// Confirmation failed. +// - Trigger a "RetryTRR" Confirmation which will start a fresh +// connection for TRR, and then retry the lookup with TRR. +// - If the second attempt failed, fallback to native if +// network.trr.strict_native_fallback is false. +// Returns true if we retried with either TRR or Native. +bool nsHostResolver::MaybeRetryTRRLookup( + AddrHostRecord* aAddrRec, nsresult aFirstAttemptStatus, + TRRSkippedReason aFirstAttemptSkipReason, nsresult aChannelStatus, + const MutexAutoLock& aLock) { + if (NS_FAILED(aFirstAttemptStatus) && + (aChannelStatus == NS_ERROR_PROXY_UNAUTHORIZED || + aChannelStatus == NS_ERROR_PROXY_AUTHENTICATION_FAILED) && + aAddrRec->mEffectiveTRRMode == nsIRequest::TRR_ONLY_MODE) { + LOG(("MaybeRetryTRRLookup retry because of proxy connect failed")); + TRRService::Get()->DontUseTRRThread(); + return DoRetryTRR(aAddrRec, aLock); + } + + if (NS_SUCCEEDED(aFirstAttemptStatus) || + aAddrRec->mEffectiveTRRMode != nsIRequest::TRR_FIRST_MODE || + aFirstAttemptStatus == NS_ERROR_DEFINITIVE_UNKNOWN_HOST) { + return false; + } + + MOZ_ASSERT(!aAddrRec->mResolving); + if (!StaticPrefs::network_trr_retry_on_recoverable_errors()) { + LOG(("nsHostResolver::MaybeRetryTRRLookup retrying with native")); + return NS_SUCCEEDED(NativeLookup(aAddrRec, aLock)); + } + + if (IsFailedConfirmationOrNoConnectivity(aFirstAttemptSkipReason) || + IsNonRecoverableTRRSkipReason(aFirstAttemptSkipReason) || + IsBlockedTRRRequest(aFirstAttemptSkipReason)) { + LOG( + ("nsHostResolver::MaybeRetryTRRLookup retrying with native in strict " + "mode, skip reason was %d", + static_cast<uint32_t>(aFirstAttemptSkipReason))); + return NS_SUCCEEDED(NativeLookup(aAddrRec, aLock)); + } + + if (aAddrRec->mTrrAttempts > 1) { + if (!StaticPrefs::network_trr_strict_native_fallback()) { + LOG( + ("nsHostResolver::MaybeRetryTRRLookup retry failed. Using " + "native.")); + return NS_SUCCEEDED(NativeLookup(aAddrRec, aLock)); + } + + if (aFirstAttemptSkipReason == TRRSkippedReason::TRR_TIMEOUT && + StaticPrefs::network_trr_strict_native_fallback_allow_timeouts()) { + LOG( + ("nsHostResolver::MaybeRetryTRRLookup retry timed out. Using " + "native.")); + return NS_SUCCEEDED(NativeLookup(aAddrRec, aLock)); + } + LOG(("nsHostResolver::MaybeRetryTRRLookup mTrrAttempts>1, not retrying.")); + return false; + } + + LOG( + ("nsHostResolver::MaybeRetryTRRLookup triggering Confirmation and " + "retrying with TRR, skip reason was %d", + static_cast<uint32_t>(aFirstAttemptSkipReason))); + TRRService::Get()->RetryTRRConfirm(); + + return DoRetryTRR(aAddrRec, aLock); +} + +bool nsHostResolver::DoRetryTRR(AddrHostRecord* aAddrRec, + const mozilla::MutexAutoLock& aLock) { + { + // Clear out the old query + auto trrQuery = aAddrRec->mTRRQuery.Lock(); + trrQuery.ref() = nullptr; + } + + if (NS_SUCCEEDED(TrrLookup(aAddrRec, aLock, nullptr /* pushedTRR */))) { + aAddrRec->NotifyRetryingTrr(); + return true; + } + + return false; +} + +// +// CompleteLookup() checks if the resolving should be redone and if so it +// returns LOOKUP_RESOLVEAGAIN, but only if 'status' is not NS_ERROR_ABORT. +nsHostResolver::LookupStatus nsHostResolver::CompleteLookup( + nsHostRecord* rec, nsresult status, AddrInfo* aNewRRSet, bool pb, + const nsACString& aOriginsuffix, TRRSkippedReason aReason, + mozilla::net::TRR* aTRRRequest) { + MutexAutoLock lock(mLock); + return CompleteLookupLocked(rec, status, aNewRRSet, pb, aOriginsuffix, + aReason, aTRRRequest, lock); +} + +nsHostResolver::LookupStatus nsHostResolver::CompleteLookupLocked( + nsHostRecord* rec, nsresult status, AddrInfo* aNewRRSet, bool pb, + const nsACString& aOriginsuffix, TRRSkippedReason aReason, + mozilla::net::TRR* aTRRRequest, const mozilla::MutexAutoLock& aLock) { + MOZ_ASSERT(rec); + MOZ_ASSERT(rec->pb == pb); + MOZ_ASSERT(rec->IsAddrRecord()); + + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + MOZ_ASSERT(addrRec); + + RefPtr<AddrInfo> newRRSet(aNewRRSet); + MOZ_ASSERT(NS_FAILED(status) || newRRSet->Addresses().Length() > 0); + + DNSResolverType type = + newRRSet ? newRRSet->ResolverType() : DNSResolverType::Native; + + if (NS_FAILED(status)) { + newRRSet = nullptr; + } + + if (addrRec->LoadResolveAgain() && (status != NS_ERROR_ABORT) && + type == DNSResolverType::Native) { + LOG(("nsHostResolver record %p resolve again due to flushcache\n", + addrRec.get())); + addrRec->StoreResolveAgain(false); + return LOOKUP_RESOLVEAGAIN; + } + + MOZ_ASSERT(addrRec->mResolving); + addrRec->mResolving--; + LOG(( + "nsHostResolver::CompleteLookup %s %p %X resolver=%d stillResolving=%d\n", + addrRec->host.get(), aNewRRSet, (unsigned int)status, (int)type, + int(addrRec->mResolving))); + + if (type != DNSResolverType::Native) { + if (NS_FAILED(status) && status != NS_ERROR_UNKNOWN_HOST && + status != NS_ERROR_DEFINITIVE_UNKNOWN_HOST) { + // the errors are not failed resolves, that means + // something else failed, consider this as *TRR not used* + // for actually trying to resolve the host + addrRec->mResolverType = DNSResolverType::Native; + } + + if (NS_FAILED(status)) { + if (aReason != TRRSkippedReason::TRR_UNSET) { + addrRec->RecordReason(aReason); + } else { + // Unknown failed reason. + addrRec->RecordReason(TRRSkippedReason::TRR_FAILED); + } + } else { + addrRec->mTRRSuccess = true; + addrRec->RecordReason(TRRSkippedReason::TRR_OK); + } + + nsresult channelStatus = aTRRRequest->ChannelStatus(); + if (MaybeRetryTRRLookup(addrRec, status, aReason, channelStatus, aLock)) { + MOZ_ASSERT(addrRec->mResolving); + return LOOKUP_OK; + } + + if (!addrRec->mTRRSuccess) { + // no TRR success + newRRSet = nullptr; + } + + if (NS_FAILED(status)) { + // This is the error that consumers expect. + status = NS_ERROR_UNKNOWN_HOST; + } + } else { // native resolve completed + if (addrRec->LoadUsingAnyThread()) { + mActiveAnyThreadCount--; + addrRec->StoreUsingAnyThread(false); + } + + addrRec->mNativeSuccess = static_cast<bool>(newRRSet); + if (addrRec->mNativeSuccess) { + addrRec->mNativeDuration = TimeStamp::Now() - addrRec->mNativeStart; + } + } + + addrRec->OnCompleteLookup(); + + // update record fields. We might have a addrRec->addr_info already if a + // previous lookup result expired and we're reresolving it or we get + // a late second TRR response. + if (!mShutdown) { + MutexAutoLock lock(addrRec->addr_info_lock); + RefPtr<AddrInfo> old_addr_info; + if (different_rrset(addrRec->addr_info, newRRSet)) { + LOG(("nsHostResolver record %p new gencnt\n", addrRec.get())); + old_addr_info = addrRec->addr_info; + addrRec->addr_info = std::move(newRRSet); + addrRec->addr_info_gencnt++; + } else { + if (addrRec->addr_info && newRRSet) { + auto builder = addrRec->addr_info->Build(); + builder.SetTTL(newRRSet->TTL()); + // Update trr timings + builder.SetTrrFetchDuration(newRRSet->GetTrrFetchDuration()); + builder.SetTrrFetchDurationNetworkOnly( + newRRSet->GetTrrFetchDurationNetworkOnly()); + + addrRec->addr_info = builder.Finish(); + } + old_addr_info = std::move(newRRSet); + } + addrRec->negative = !addrRec->addr_info; + PrepareRecordExpirationAddrRecord(addrRec); + } + + if (LOG_ENABLED()) { + MutexAutoLock lock(addrRec->addr_info_lock); + if (addrRec->addr_info) { + for (const auto& elem : addrRec->addr_info->Addresses()) { + char buf[128]; + elem.ToStringBuffer(buf, sizeof(buf)); + LOG(("CompleteLookup: %s has %s\n", addrRec->host.get(), buf)); + } + } else { + LOG(("CompleteLookup: %s has NO address\n", addrRec->host.get())); + } + } + + // get the list of pending callbacks for this lookup, and notify + // them that the lookup is complete. + mozilla::LinkedList<RefPtr<nsResolveHostCallback>> cbs = + std::move(rec->mCallbacks); + + LOG(("nsHostResolver record %p calling back dns users status:%X\n", + addrRec.get(), int(status))); + + for (nsResolveHostCallback* c = cbs.getFirst(); c; + c = c->removeAndGetNext()) { + c->OnResolveHostComplete(this, rec, status); + } + + if (!addrRec->mResolving && !mShutdown) { + { + auto trrQuery = addrRec->mTRRQuery.Lock(); + if (trrQuery.ref()) { + addrRec->mTrrDuration = trrQuery.ref()->Duration(); + } + trrQuery.ref() = nullptr; + } + addrRec->ResolveComplete(); + + AddToEvictionQ(rec, aLock); + } + +#ifdef DNSQUERY_AVAILABLE + // Unless the result is from TRR, resolve again to get TTL + bool hasNativeResult = false; + { + MutexAutoLock lock(addrRec->addr_info_lock); + if (addrRec->addr_info && !addrRec->addr_info->IsTRR()) { + hasNativeResult = true; + } + } + if (hasNativeResult && !mShutdown && !addrRec->LoadGetTtl() && + !rec->mResolving && sGetTtlEnabled) { + LOG(("Issuing second async lookup for TTL for host [%s].", + addrRec->host.get())); + addrRec->flags = + (addrRec->flags & ~nsIDNSService::RESOLVE_PRIORITY_MEDIUM) | + nsIDNSService::RESOLVE_PRIORITY_LOW; + DebugOnly<nsresult> rv = NativeLookup(rec, aLock); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Could not issue second async lookup for TTL."); + } +#endif + return LOOKUP_OK; +} + +nsHostResolver::LookupStatus nsHostResolver::CompleteLookupByType( + nsHostRecord* rec, nsresult status, + mozilla::net::TypeRecordResultType& aResult, uint32_t aTtl, bool pb) { + MutexAutoLock lock(mLock); + return CompleteLookupByTypeLocked(rec, status, aResult, aTtl, pb, lock); +} + +nsHostResolver::LookupStatus nsHostResolver::CompleteLookupByTypeLocked( + nsHostRecord* rec, nsresult status, + mozilla::net::TypeRecordResultType& aResult, uint32_t aTtl, bool pb, + const mozilla::MutexAutoLock& aLock) { + MOZ_ASSERT(rec); + MOZ_ASSERT(rec->pb == pb); + MOZ_ASSERT(!rec->IsAddrRecord()); + + RefPtr<TypeHostRecord> typeRec = do_QueryObject(rec); + MOZ_ASSERT(typeRec); + + MOZ_ASSERT(typeRec->mResolving); + typeRec->mResolving--; + + { + auto lock = rec->mTRRQuery.Lock(); + lock.ref() = nullptr; + } + + uint32_t duration = static_cast<uint32_t>( + (TimeStamp::Now() - typeRec->mStart).ToMilliseconds()); + + if (NS_FAILED(status)) { + LOG(("nsHostResolver::CompleteLookupByType record %p [%s] status %x\n", + typeRec.get(), typeRec->host.get(), (unsigned int)status)); + typeRec->SetExpiration( + TimeStamp::NowLoRes(), + StaticPrefs::network_dns_negative_ttl_for_type_record(), 0); + MOZ_ASSERT(aResult.is<TypeRecordEmpty>()); + status = NS_ERROR_UNKNOWN_HOST; + typeRec->negative = true; + Telemetry::Accumulate(Telemetry::DNS_BY_TYPE_FAILED_LOOKUP_TIME, duration); + } else { + size_t recordCount = 0; + if (aResult.is<TypeRecordTxt>()) { + recordCount = aResult.as<TypeRecordTxt>().Length(); + } else if (aResult.is<TypeRecordHTTPSSVC>()) { + recordCount = aResult.as<TypeRecordHTTPSSVC>().Length(); + } + LOG( + ("nsHostResolver::CompleteLookupByType record %p [%s], number of " + "records %zu\n", + typeRec.get(), typeRec->host.get(), recordCount)); + MutexAutoLock typeLock(typeRec->mResultsLock); + typeRec->mResults = aResult; + typeRec->SetExpiration(TimeStamp::NowLoRes(), aTtl, mDefaultGracePeriod); + typeRec->negative = false; + Telemetry::Accumulate(Telemetry::DNS_BY_TYPE_SUCCEEDED_LOOKUP_TIME, + duration); + } + + mozilla::LinkedList<RefPtr<nsResolveHostCallback>> cbs = + std::move(typeRec->mCallbacks); + + LOG( + ("nsHostResolver::CompleteLookupByType record %p calling back dns " + "users\n", + typeRec.get())); + + for (nsResolveHostCallback* c = cbs.getFirst(); c; + c = c->removeAndGetNext()) { + c->OnResolveHostComplete(this, rec, status); + } + + AddToEvictionQ(rec, aLock); + return LOOKUP_OK; +} + +void nsHostResolver::CancelAsyncRequest( + const nsACString& host, const nsACString& aTrrServer, uint16_t aType, + const OriginAttributes& aOriginAttributes, nsIDNSService::DNSFlags flags, + uint16_t af, nsIDNSListener* aListener, nsresult status) + +{ + MutexAutoLock lock(mLock); + + nsAutoCString originSuffix; + aOriginAttributes.CreateSuffix(originSuffix); + + // Lookup the host record associated with host, flags & address family + + nsHostKey key(host, aTrrServer, aType, flags, af, + (aOriginAttributes.mPrivateBrowsingId > 0), originSuffix); + RefPtr<nsHostRecord> rec = mRecordDB.Get(key); + if (!rec) { + return; + } + + for (RefPtr<nsResolveHostCallback> c : rec->mCallbacks) { + if (c->EqualsAsyncListener(aListener)) { + c->remove(); + c->OnResolveHostComplete(this, rec.get(), status); + break; + } + } + + // If there are no more callbacks, remove the hash table entry + if (rec->mCallbacks.isEmpty()) { + mRecordDB.Remove(*static_cast<nsHostKey*>(rec.get())); + // If record is on a Queue, remove it + mQueue.MaybeRemoveFromQ(rec, lock); + } +} + +size_t nsHostResolver::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) const { + MutexAutoLock lock(mLock); + + size_t n = mallocSizeOf(this); + + n += mRecordDB.ShallowSizeOfExcludingThis(mallocSizeOf); + for (const auto& entry : mRecordDB.Values()) { + n += entry->SizeOfIncludingThis(mallocSizeOf); + } + + // The following fields aren't measured. + // - mHighQ, mMediumQ, mLowQ, mEvictionQ, because they just point to + // nsHostRecords that also pointed to by entries |mRecordDB|, and + // measured when |mRecordDB| is measured. + + return n; +} + +void nsHostResolver::ThreadFunc() { + LOG(("DNS lookup thread - starting execution.\n")); + +#if defined(RES_RETRY_ON_FAILURE) + nsResState rs; +#endif + RefPtr<AddrHostRecord> rec; + RefPtr<AddrInfo> ai; + + do { + if (!rec) { + RefPtr<AddrHostRecord> tmpRec; + if (!GetHostToLookup(getter_AddRefs(tmpRec))) { + break; // thread shutdown signal + } + // GetHostToLookup() returns an owning reference + MOZ_ASSERT(tmpRec); + rec.swap(tmpRec); + } + + LOG1(("DNS lookup thread - Calling getaddrinfo for host [%s].\n", + rec->host.get())); + + TimeStamp startTime = TimeStamp::Now(); + bool getTtl = rec->LoadGetTtl(); + TimeDuration inQueue = startTime - rec->mNativeStart; + uint32_t ms = static_cast<uint32_t>(inQueue.ToMilliseconds()); + Telemetry::Accumulate(Telemetry::DNS_NATIVE_QUEUING, ms); + nsresult status = + GetAddrInfo(rec->host, rec->af, rec->flags, getter_AddRefs(ai), getTtl); +#if defined(RES_RETRY_ON_FAILURE) + if (NS_FAILED(status) && rs.Reset()) { + status = GetAddrInfo(rec->host, rec->af, rec->flags, getter_AddRefs(ai), + getTtl); + } +#endif + + { // obtain lock to check shutdown and manage inter-module telemetry + MutexAutoLock lock(mLock); + + if (!mShutdown) { + TimeDuration elapsed = TimeStamp::Now() - startTime; + uint32_t millis = static_cast<uint32_t>(elapsed.ToMilliseconds()); + + if (NS_SUCCEEDED(status)) { + Telemetry::HistogramID histogramID; + if (!rec->addr_info_gencnt) { + // Time for initial lookup. + histogramID = Telemetry::DNS_LOOKUP_TIME; + } else if (!getTtl) { + // Time for renewal; categorized by expiration strategy. + histogramID = Telemetry::DNS_RENEWAL_TIME; + } else { + // Time to get TTL; categorized by expiration strategy. + histogramID = Telemetry::DNS_RENEWAL_TIME_FOR_TTL; + } + Telemetry::Accumulate(histogramID, millis); + } else { + Telemetry::Accumulate(Telemetry::DNS_FAILED_LOOKUP_TIME, millis); + } + } + } + + LOG1(("DNS lookup thread - lookup completed for host [%s]: %s.\n", + rec->host.get(), ai ? "success" : "failure: unknown host")); + + if (LOOKUP_RESOLVEAGAIN == + CompleteLookup(rec, status, ai, rec->pb, rec->originSuffix, + rec->mTRRSkippedReason, nullptr)) { + // leave 'rec' assigned and loop to make a renewed host resolve + LOG(("DNS lookup thread - Re-resolving host [%s].\n", rec->host.get())); + } else { + rec = nullptr; + } + } while (true); + + MutexAutoLock lock(mLock); + mActiveTaskCount--; + LOG(("DNS lookup thread - queue empty, task finished.\n")); +} + +void nsHostResolver::SetCacheLimits(uint32_t aMaxCacheEntries, + uint32_t aDefaultCacheEntryLifetime, + uint32_t aDefaultGracePeriod) { + MutexAutoLock lock(mLock); + mMaxCacheEntries = aMaxCacheEntries; + mDefaultCacheLifetime = aDefaultCacheEntryLifetime; + mDefaultGracePeriod = aDefaultGracePeriod; +} + +nsresult nsHostResolver::Create(uint32_t maxCacheEntries, + uint32_t defaultCacheEntryLifetime, + uint32_t defaultGracePeriod, + nsHostResolver** result) { + RefPtr<nsHostResolver> res = new nsHostResolver( + maxCacheEntries, defaultCacheEntryLifetime, defaultGracePeriod); + + nsresult rv = res->Init(); + if (NS_FAILED(rv)) { + return rv; + } + + res.forget(result); + return NS_OK; +} + +void nsHostResolver::GetDNSCacheEntries(nsTArray<DNSCacheEntries>* args) { + MutexAutoLock lock(mLock); + for (const auto& recordEntry : mRecordDB) { + // We don't pay attention to address literals, only resolved domains. + // Also require a host. + nsHostRecord* rec = recordEntry.GetWeak(); + MOZ_ASSERT(rec, "rec should never be null here!"); + + if (!rec) { + continue; + } + + // For now we only show A/AAAA records. + if (!rec->IsAddrRecord()) { + continue; + } + + RefPtr<AddrHostRecord> addrRec = do_QueryObject(rec); + MOZ_ASSERT(addrRec); + if (!addrRec || !addrRec->addr_info) { + continue; + } + + DNSCacheEntries info; + info.hostname = rec->host; + info.family = rec->af; + info.expiration = + (int64_t)(rec->mValidEnd - TimeStamp::NowLoRes()).ToSeconds(); + if (info.expiration <= 0) { + // We only need valid DNS cache entries + continue; + } + + { + MutexAutoLock lock(addrRec->addr_info_lock); + for (const auto& addr : addrRec->addr_info->Addresses()) { + char buf[kIPv6CStrBufSize]; + if (addr.ToStringBuffer(buf, sizeof(buf))) { + info.hostaddr.AppendElement(buf); + } + } + info.TRR = addrRec->addr_info->IsTRR(); + } + + info.originAttributesSuffix = recordEntry.GetKey().originSuffix; + info.flags = nsPrintfCString("%u|0x%x|%u|%d|%s", rec->type, rec->flags, + rec->af, rec->pb, rec->mTrrServer.get()); + + args->AppendElement(std::move(info)); + } +} + +#undef LOG +#undef LOG_ENABLED |