/* vim:set ts=4 sw=4 sts=4 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 #include #include #include #include #define RES_RETRY_ON_FAILURE #endif #include #include #include "nsHostResolver.h" #include "nsError.h" #include "nsISupportsBase.h" #include "nsISupportsUtils.h" #include "nsAutoPtr.h" #include "nsPrintfCString.h" #include "prthread.h" #include "prerror.h" #include "prtime.h" #include "mozilla/Logging.h" #include "PLDHashTable.h" #include "plstr.h" #include "nsURLHelper.h" #include "nsThreadUtils.h" #include "GetAddrInfo.h" #include "mozilla/HashFunctions.h" #include "mozilla/TimeStamp.h" #include "mozilla/Telemetry.h" #include "mozilla/DebugOnly.h" #include "mozilla/Preferences.h" 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 MAX_RESOLVER_THREADS in size. New requests // go first to an idle thread. If that cannot be found and there are fewer than MAX_RESOLVER_THREADS // 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 HighThreadThreshold 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 HighThreadThreshold in size a thread will be destroyed after // ShortIdleTimeoutSeconds of idle time. Smaller pools use LongIdleTimeoutSeconds for a // timeout period. #define HighThreadThreshold MAX_RESOLVER_THREADS_FOR_ANY_PRIORITY #define LongIdleTimeoutSeconds 300 // for threads 1 -> HighThreadThreshold #define ShortIdleTimeoutSeconds 60 // for threads HighThreadThreshold+1 -> MAX_RESOLVER_THREADS PR_STATIC_ASSERT (HighThreadThreshold <= MAX_RESOLVER_THREADS); //---------------------------------------------------------------------------- static LazyLogModule gHostResolverLog("nsHostResolver"); #define LOG(args) MOZ_LOG(gHostResolverLog, mozilla::LogLevel::Debug, args) #define LOG_HOST(host, interface) host, \ (interface && interface[0] != '\0') ? " on interface " : "", \ (interface && interface[0] != '\0') ? interface : "" //---------------------------------------------------------------------------- static inline void MoveCList(PRCList &from, PRCList &to) { if (!PR_CLIST_IS_EMPTY(&from)) { to.next = from.next; to.prev = from.prev; to.next->prev = &to; to.prev->next = &to; PR_INIT_CLIST(&from); } } //---------------------------------------------------------------------------- #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; LOG(("Calling 'res_ninit'.\n")); mLastReset = PR_IntervalNow(); return (res_ninit(&_res) == 0); } private: PRIntervalTime mLastReset; }; #endif // RES_RETRY_ON_FAILURE //---------------------------------------------------------------------------- static inline bool IsHighPriority(uint16_t flags) { return !(flags & (nsHostResolver::RES_PRIORITY_LOW | nsHostResolver::RES_PRIORITY_MEDIUM)); } static inline bool IsMediumPriority(uint16_t flags) { return flags & nsHostResolver::RES_PRIORITY_MEDIUM; } static inline bool IsLowPriority(uint16_t flags) { return flags & nsHostResolver::RES_PRIORITY_LOW; } //---------------------------------------------------------------------------- // this macro filters out any flags that are not used when constructing the // host key. the significant flags are those that would affect the resulting // host record (i.e., the flags that are passed down to PR_GetAddrInfoByName). #define RES_KEY_FLAGS(_f) ((_f) & nsHostResolver::RES_CANON_NAME) nsHostRecord::nsHostRecord(const nsHostKey *key) : addr_info_lock("nsHostRecord.addr_info_lock") , addr_info_gencnt(0) , addr_info(nullptr) , addr(nullptr) , negative(false) , resolving(false) , onQueue(false) , usingAnyThread(false) , mDoomed(false) #if TTL_AVAILABLE , mGetTtl(false) #endif , mBlacklistedCount(0) , mResolveAgain(false) { host = ((char *) this) + sizeof(nsHostRecord); memcpy((char *) host, key->host, strlen(key->host) + 1); flags = key->flags; af = key->af; netInterface = host + strlen(key->host) + 1; memcpy((char *) netInterface, key->netInterface, strlen(key->netInterface) + 1); PR_INIT_CLIST(this); PR_INIT_CLIST(&callbacks); } nsresult nsHostRecord::Create(const nsHostKey *key, nsHostRecord **result) { size_t hostLen = strlen(key->host) + 1; size_t netInterfaceLen = strlen(key->netInterface) + 1; size_t size = hostLen + netInterfaceLen + sizeof(nsHostRecord); // Use placement new to create the object with room for the hostname and // network interface name allocated after it. void *place = ::operator new(size); *result = new(place) nsHostRecord(key); NS_ADDREF(*result); return NS_OK; } void nsHostRecord::SetExpiration(const mozilla::TimeStamp& now, unsigned int valid, unsigned int grace) { mValidStart = now; mGraceStart = now + TimeDuration::FromSeconds(valid); mValidEnd = now + TimeDuration::FromSeconds(valid + grace); } void nsHostRecord::CopyExpirationTimesAndFlagsFrom(const nsHostRecord *aFromHostRecord) { // This is used to copy information from a cache entry to a record. All // information necessary for HasUsableRecord needs to be copied. mValidStart = aFromHostRecord->mValidStart; mValidEnd = aFromHostRecord->mValidEnd; mGraceStart = aFromHostRecord->mGraceStart; mDoomed = aFromHostRecord->mDoomed; } nsHostRecord::~nsHostRecord() { Telemetry::Accumulate(Telemetry::DNS_BLACKLIST_COUNT, mBlacklistedCount); delete addr_info; delete addr; } bool nsHostRecord::Blacklisted(NetAddr *aQuery) { // must call locked LOG(("Checking blacklist for host [%s%s%s], host record [%p].\n", LOG_HOST(host, netInterface), this)); // skip the string conversion for the common case of no blacklist if (!mBlacklistedItems.Length()) { return false; } char buf[kIPv6CStrBufSize]; if (!NetAddrToString(aQuery, buf, sizeof(buf))) { return false; } nsDependentCString strQuery(buf); for (uint32_t i = 0; i < mBlacklistedItems.Length(); i++) { if (mBlacklistedItems.ElementAt(i).Equals(strQuery)) { LOG(("Address [%s] is blacklisted for host [%s%s%s].\n", buf, LOG_HOST(host, netInterface))); return true; } } return false; } void nsHostRecord::ReportUnusable(NetAddr *aAddress) { // must call locked LOG(("Adding address to blacklist for host [%s%s%s], host record [%p].\n", LOG_HOST(host, netInterface), this)); ++mBlacklistedCount; if (negative) mDoomed = true; char buf[kIPv6CStrBufSize]; if (NetAddrToString(aAddress, buf, sizeof(buf))) { LOG(("Successfully adding address [%s] to blacklist for host " "[%s%s%s].\n", buf, LOG_HOST(host, netInterface))); mBlacklistedItems.AppendElement(nsCString(buf)); } } void nsHostRecord::ResetBlacklist() { // must call locked LOG(("Resetting blacklist for host [%s%s%s], host record [%p].\n", LOG_HOST(host, netInterface), this)); mBlacklistedItems.Clear(); } nsHostRecord::ExpirationStatus nsHostRecord::CheckExpiration(const mozilla::TimeStamp& now) const { if (!mGraceStart.IsNull() && now >= mGraceStart && !mValidEnd.IsNull() && now < mValidEnd) { return nsHostRecord::EXP_GRACE; } else if (!mValidEnd.IsNull() && now < mValidEnd) { return nsHostRecord::EXP_VALID; } return nsHostRecord::EXP_EXPIRED; } bool nsHostRecord::HasUsableResult(const mozilla::TimeStamp& now, uint16_t queryFlags) const { if (mDoomed) { return false; } // don't use cached negative results for high priority queries. if (negative && IsHighPriority(queryFlags)) { return false; } if (CheckExpiration(now) == EXP_EXPIRED) { return false; } return addr_info || addr || negative; } static size_t SizeOfResolveHostCallbackListExcludingHead(const PRCList *head, MallocSizeOf mallocSizeOf) { size_t n = 0; PRCList *curr = head->next; while (curr != head) { nsResolveHostCallback *callback = static_cast(curr); n += callback->SizeOfIncludingThis(mallocSizeOf); curr = curr->next; } return n; } size_t nsHostRecord::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) const { size_t n = mallocSizeOf(this); // The |host| field (inherited from nsHostKey) actually points to extra // memory that is allocated beyond the end of the nsHostRecord (see // nsHostRecord::Create()). So it will be included in the // |mallocSizeOf(this)| call above. n += SizeOfResolveHostCallbackListExcludingHead(&callbacks, mallocSizeOf); n += addr_info ? addr_info->SizeOfIncludingThis(mallocSizeOf) : 0; n += mallocSizeOf(addr); n += mBlacklistedItems.ShallowSizeOfExcludingThis(mallocSizeOf); for (size_t i = 0; i < mBlacklistedItems.Length(); i++) { n += mBlacklistedItems[i].SizeOfExcludingThisIfUnshared(mallocSizeOf); } return n; } nsHostRecord::DnsPriority nsHostRecord::GetPriority(uint16_t aFlags) { if (IsHighPriority(aFlags)){ return nsHostRecord::DNS_PRIORITY_HIGH; } else if (IsMediumPriority(aFlags)) { return nsHostRecord::DNS_PRIORITY_MEDIUM; } return nsHostRecord::DNS_PRIORITY_LOW; } // Returns true if the entry can be removed, or false if it should be left. // Sets mResolveAgain true for entries being resolved right now. bool nsHostRecord::RemoveOrRefresh() { if (resolving) { if (!onQueue) { // The request has been passed to the OS resolver. The resultant DNS // record should be considered stale and not trusted; set a flag to // ensure it is called again. mResolveAgain = true; } // if Onqueue is true, the host entry is already added to the cache // but is still pending to get resolved: just leave it in hash. return false; } // Already resolved; not in a pending state; remove from cache. return true; } //---------------------------------------------------------------------------- struct nsHostDBEnt : PLDHashEntryHdr { nsHostRecord *rec; }; static PLDHashNumber HostDB_HashKey(PLDHashTable *table, const void *key) { const nsHostKey *hk = static_cast(key); return AddToHash(HashString(hk->host), RES_KEY_FLAGS(hk->flags), hk->af, HashString(hk->netInterface)); } static bool HostDB_MatchEntry(PLDHashTable *table, const PLDHashEntryHdr *entry, const void *key) { const nsHostDBEnt *he = static_cast(entry); const nsHostKey *hk = static_cast(key); return !strcmp(he->rec->host, hk->host) && RES_KEY_FLAGS (he->rec->flags) == RES_KEY_FLAGS(hk->flags) && he->rec->af == hk->af && !strcmp(he->rec->netInterface, hk->netInterface); } static void HostDB_MoveEntry(PLDHashTable *table, const PLDHashEntryHdr *from, PLDHashEntryHdr *to) { static_cast(to)->rec = static_cast(from)->rec; } static void HostDB_ClearEntry(PLDHashTable *table, PLDHashEntryHdr *entry) { nsHostDBEnt *he = static_cast(entry); MOZ_ASSERT(he, "nsHostDBEnt is null!"); nsHostRecord *hr = he->rec; MOZ_ASSERT(hr, "nsHostDBEnt has null host record!"); LOG(("Clearing cache db entry for host [%s%s%s].\n", LOG_HOST(hr->host, hr->netInterface))); #if defined(DEBUG) { MutexAutoLock lock(hr->addr_info_lock); if (!hr->addr_info) { LOG(("No address info for host [%s%s%s].\n", LOG_HOST(hr->host, hr->netInterface))); } else { if (!hr->mValidEnd.IsNull()) { TimeDuration diff = hr->mValidEnd - TimeStamp::NowLoRes(); LOG(("Record for host [%s%s%s] expires in %f seconds.\n", LOG_HOST(hr->host, hr->netInterface), diff.ToSeconds())); } else { LOG(("Record for host [%s%s%s] not yet valid.\n", LOG_HOST(hr->host, hr->netInterface))); } NetAddrElement *addrElement = nullptr; char buf[kIPv6CStrBufSize]; do { if (!addrElement) { addrElement = hr->addr_info->mAddresses.getFirst(); } else { addrElement = addrElement->getNext(); } if (addrElement) { NetAddrToString(&addrElement->mAddress, buf, sizeof(buf)); LOG((" [%s]\n", buf)); } } while (addrElement); } } #endif NS_RELEASE(he->rec); } static void HostDB_InitEntry(PLDHashEntryHdr *entry, const void *key) { nsHostDBEnt *he = static_cast(entry); nsHostRecord::Create(static_cast(key), &he->rec); } static const PLDHashTableOps gHostDB_ops = { HostDB_HashKey, HostDB_MatchEntry, HostDB_MoveEntry, HostDB_ClearEntry, HostDB_InitEntry, }; //---------------------------------------------------------------------------- #if TTL_AVAILABLE static const char kPrefGetTtl[] = "network.dns.get-ttl"; static bool sGetTtlEnabled = false; static void DnsPrefChanged(const char* aPref, void* aClosure) { MOZ_ASSERT(NS_IsMainThread(), "Should be getting pref changed notification on main thread!"); if (strcmp(aPref, kPrefGetTtl) != 0) { LOG(("DnsPrefChanged ignoring pref \"%s\"", aPref)); return; } auto self = static_cast(aClosure); MOZ_ASSERT(self); sGetTtlEnabled = Preferences::GetBool(kPrefGetTtl); } #endif nsHostResolver::nsHostResolver(uint32_t maxCacheEntries, uint32_t defaultCacheEntryLifetime, uint32_t defaultGracePeriod) : mMaxCacheEntries(maxCacheEntries) , mDefaultCacheLifetime(defaultCacheEntryLifetime) , mDefaultGracePeriod(defaultGracePeriod) , mLock("nsHostResolver.mLock") , mIdleThreadCV(mLock, "nsHostResolver.mIdleThreadCV") , mDB(&gHostDB_ops, sizeof(nsHostDBEnt), 0) , mEvictionQSize(0) , mShutdown(true) , mNumIdleThreads(0) , mThreadCount(0) , mActiveAnyThreadCount(0) , mPendingCount(0) { mCreationTime = PR_Now(); PR_INIT_CLIST(&mHighQ); PR_INIT_CLIST(&mMediumQ); PR_INIT_CLIST(&mLowQ); PR_INIT_CLIST(&mEvictionQ); mLongIdleTimeout = PR_SecondsToInterval(LongIdleTimeoutSeconds); mShortIdleTimeout = PR_SecondsToInterval(ShortIdleTimeoutSeconds); } nsHostResolver::~nsHostResolver() { } nsresult nsHostResolver::Init() { if (NS_FAILED(GetAddrInfoInit())) { return NS_ERROR_FAILURE; } mShutdown = false; #if TTL_AVAILABLE // 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 rv = Preferences::RegisterCallbackAndCall( &DnsPrefChanged, kPrefGetTtl, this); NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Could not register DNS TTL pref callback."); } #endif #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) { LOG(("Calling 'res_ninit'.\n")); res_ninit(&_res); } #endif return NS_OK; } void nsHostResolver::ClearPendingQueue(PRCList *aPendingQ) { // loop through pending queue, erroring out pending lookups. if (!PR_CLIST_IS_EMPTY(aPendingQ)) { PRCList *node = aPendingQ->next; while (node != aPendingQ) { nsHostRecord *rec = static_cast(node); node = node->next; OnLookupComplete(rec, NS_ERROR_ABORT, nullptr); } } } // // 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() { MutexAutoLock lock(mLock); mEvictionQSize = 0; // Clear the evictionQ and remove all its corresponding entries from // the cache first if (!PR_CLIST_IS_EMPTY(&mEvictionQ)) { PRCList *node = mEvictionQ.next; while (node != &mEvictionQ) { nsHostRecord *rec = static_cast(node); node = node->next; PR_REMOVE_AND_INIT_LINK(rec); mDB.Remove((nsHostKey *) rec); NS_RELEASE(rec); } } // Refresh the cache entries that are resolving RIGHT now, remove the rest. for (auto iter = mDB.Iter(); !iter.Done(); iter.Next()) { auto entry = static_cast(iter.Get()); // Try to remove the record, or mark it for refresh. if (entry->rec->RemoveOrRefresh()) { PR_REMOVE_LINK(entry->rec); iter.Remove(); } } } void nsHostResolver::Shutdown() { LOG(("Shutting down host resolver.\n")); #if TTL_AVAILABLE { DebugOnly rv = Preferences::UnregisterCallback( &DnsPrefChanged, kPrefGetTtl, this); NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Could not unregister DNS TTL pref callback."); } #endif PRCList pendingQHigh, pendingQMed, pendingQLow, evictionQ; PR_INIT_CLIST(&pendingQHigh); PR_INIT_CLIST(&pendingQMed); PR_INIT_CLIST(&pendingQLow); PR_INIT_CLIST(&evictionQ); { MutexAutoLock lock(mLock); mShutdown = true; MoveCList(mHighQ, pendingQHigh); MoveCList(mMediumQ, pendingQMed); MoveCList(mLowQ, pendingQLow); MoveCList(mEvictionQ, evictionQ); mEvictionQSize = 0; mPendingCount = 0; if (mNumIdleThreads) mIdleThreadCV.NotifyAll(); // empty host database mDB.Clear(); } ClearPendingQueue(&pendingQHigh); ClearPendingQueue(&pendingQMed); ClearPendingQueue(&pendingQLow); if (!PR_CLIST_IS_EMPTY(&evictionQ)) { PRCList *node = evictionQ.next; while (node != &evictionQ) { nsHostRecord *rec = static_cast(node); node = node->next; NS_RELEASE(rec); } } #ifdef NS_BUILD_REFCNT_LOGGING // Logically join the outstanding worker threads with a timeout. // Use this approach instead of PR_JoinThread() because that does // not allow a timeout which may be necessary for a semi-responsive // shutdown if the thread is blocked on a very slow DNS resolution. // mThreadCount is read outside of mLock, but the worst case // scenario for that race is one extra 25ms sleep. PRIntervalTime delay = PR_MillisecondsToInterval(25); PRIntervalTime stopTime = PR_IntervalNow() + PR_SecondsToInterval(20); while (mThreadCount && PR_IntervalNow() < stopTime) PR_Sleep(delay); #endif { mozilla::DebugOnly rv = GetAddrInfoShutdown(); NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to shutdown GetAddrInfo"); } } void nsHostResolver::MoveQueue(nsHostRecord *aRec, PRCList &aDestQ) { NS_ASSERTION(aRec->onQueue, "Moving Host Record Not Currently Queued"); PR_REMOVE_LINK(aRec); PR_APPEND_LINK(aRec, &aDestQ); } nsresult nsHostResolver::ResolveHost(const char *host, uint16_t flags, uint16_t af, const char *netInterface, nsResolveHostCallback *callback) { NS_ENSURE_TRUE(host && *host, NS_ERROR_UNEXPECTED); NS_ENSURE_TRUE(netInterface, NS_ERROR_UNEXPECTED); LOG(("Resolving host [%s%s%s]%s.\n", LOG_HOST(host, netInterface), flags & RES_BYPASS_CACHE ? " - bypassing cache" : "")); // ensure that we are working with a valid hostname before proceeding. see // bug 304904 for details. if (!net_IsValidHostName(nsDependentCString(host))) return NS_ERROR_UNKNOWN_HOST; // if result is set inside the lock, then we need to issue the // callback before returning. RefPtr result; nsresult status = NS_OK, rv = NS_OK; { MutexAutoLock lock(mLock); if (mShutdown) rv = NS_ERROR_NOT_INITIALIZED; else { // Used to try to parse to an IP address literal. PRNetAddr tempAddr; // Unfortunately, PR_StringToNetAddr does not properly initialize // the output buffer in the case of IPv6 input. See bug 223145. memset(&tempAddr, 0, sizeof(PRNetAddr)); // 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. nsHostKey key = { host, flags, af, netInterface }; auto he = static_cast(mDB.Add(&key, fallible)); // if the record is null, the hash table OOM'd. if (!he) { LOG((" Out of memory: no cache entry for host [%s%s%s].\n", LOG_HOST(host, netInterface))); rv = NS_ERROR_OUT_OF_MEMORY; } // do we have a cached result that we can reuse? else if (!(flags & RES_BYPASS_CACHE) && he->rec->HasUsableResult(TimeStamp::NowLoRes(), flags)) { LOG((" Using cached record for host [%s%s%s].\n", LOG_HOST(host, netInterface))); // put reference to host record on stack... result = he->rec; 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(he->rec, host); if (he->rec->negative) { LOG((" Negative cache entry for host [%s%s%s].\n", LOG_HOST(host, netInterface))); Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_NEGATIVE_HIT); status = NS_ERROR_UNKNOWN_HOST; } } // if the host name is an IP address literal and has been parsed, // go ahead and use it. else if (he->rec->addr) { LOG((" Using cached address for IP Literal [%s].\n", host)); Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_LITERAL); result = he->rec; } // 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.) else if (PR_StringToNetAddr(host, &tempAddr) == PR_SUCCESS) { LOG((" Host is IP Literal [%s].\n", host)); // ok, just copy the result into the host record, and be done // with it! ;-) he->rec->addr = new NetAddr(); PRNetAddrToNetAddr(&tempAddr, he->rec->addr); // put reference to host record on stack... Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_LITERAL); result = he->rec; } else if (mPendingCount >= MAX_NON_PRIORITY_REQUESTS && !IsHighPriority(flags) && !he->rec->resolving) { LOG((" Lookup queue full: dropping %s priority request for " "host [%s%s%s].\n", IsMediumPriority(flags) ? "medium" : "low", LOG_HOST(host, netInterface))); 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; } else if (flags & RES_OFFLINE) { LOG((" Offline request for host [%s%s%s]; ignoring.\n", LOG_HOST(host, netInterface))); rv = NS_ERROR_OFFLINE; } // If this is an IPV4 or IPV6 specific request, check if there is // an AF_UNSPEC entry we can use. Otherwise, hit the resolver... else if (!he->rec->resolving) { if (!(flags & RES_BYPASS_CACHE) && ((af == PR_AF_INET) || (af == PR_AF_INET6))) { // First, search for an entry with AF_UNSPEC const nsHostKey unspecKey = { host, flags, PR_AF_UNSPEC, netInterface }; auto unspecHe = static_cast(mDB.Search(&unspecKey)); NS_ASSERTION(!unspecHe || (unspecHe && unspecHe->rec), "Valid host entries should contain a record"); TimeStamp now = TimeStamp::NowLoRes(); if (unspecHe && unspecHe->rec->HasUsableResult(now, flags)) { MOZ_ASSERT(unspecHe->rec->addr_info || unspecHe->rec->negative, "Entry should be resolved or negative."); LOG((" Trying AF_UNSPEC entry for host [%s%s%s] af: %s.\n", LOG_HOST(host, netInterface), (af == PR_AF_INET) ? "AF_INET" : "AF_INET6")); he->rec->addr_info = nullptr; if (unspecHe->rec->negative) { he->rec->negative = unspecHe->rec->negative; he->rec->CopyExpirationTimesAndFlagsFrom(unspecHe->rec); } else if (unspecHe->rec->addr_info) { // Search for any valid address in the AF_UNSPEC entry // in the cache (not blacklisted and from the right // family). NetAddrElement *addrIter = unspecHe->rec->addr_info->mAddresses.getFirst(); while (addrIter) { if ((af == addrIter->mAddress.inet.family) && !unspecHe->rec->Blacklisted(&addrIter->mAddress)) { if (!he->rec->addr_info) { he->rec->addr_info = new AddrInfo( unspecHe->rec->addr_info->mHostName, unspecHe->rec->addr_info->mCanonicalName); he->rec->CopyExpirationTimesAndFlagsFrom(unspecHe->rec); } he->rec->addr_info->AddAddress( new NetAddrElement(*addrIter)); } addrIter = addrIter->getNext(); } } // Now check if we have a new record. if (he->rec->HasUsableResult(now, flags)) { result = he->rec; if (he->rec->negative) { status = NS_ERROR_UNKNOWN_HOST; } Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_HIT); ConditionallyRefreshRecord(he->rec, host); } // 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. else if (af == PR_AF_INET6) { LOG((" No AF_INET6 in AF_UNSPEC entry: " "host [%s%s%s] unknown host.", LOG_HOST(host, netInterface))); result = he->rec; he->rec->negative = true; status = NS_ERROR_UNKNOWN_HOST; Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_NEGATIVE_HIT); } } } // If no valid address was found in the cache or this is an // AF_UNSPEC request, then start a new lookup. if (!result) { LOG((" No usable address in cache for host [%s%s%s].", LOG_HOST(host, netInterface))); // Add callback to the list of pending callbacks. PR_APPEND_LINK(callback, &he->rec->callbacks); he->rec->flags = flags; rv = IssueLookup(he->rec); Telemetry::Accumulate(Telemetry::DNS_LOOKUP_METHOD2, METHOD_NETWORK_FIRST); if (NS_FAILED(rv)) { PR_REMOVE_AND_INIT_LINK(callback); } else { LOG((" DNS lookup for host [%s%s%s] blocking " "pending 'getaddrinfo' query: callback [%p]", LOG_HOST(host, netInterface), callback)); } } } else { LOG((" Host [%s%s%s] is being resolved. Appending callback " "[%p].", LOG_HOST(host, netInterface), callback)); PR_APPEND_LINK(callback, &he->rec->callbacks); if (he->rec->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(he->rec->flags)) { // Move from (low|med) to high. MoveQueue(he->rec, mHighQ); he->rec->flags = flags; ConditionallyCreateThread(he->rec); } else if (IsMediumPriority(flags) && IsLowPriority(he->rec->flags)) { // Move from low to med. MoveQueue(he->rec, mMediumQ); he->rec->flags = flags; mIdleThreadCV.Notify(); } } } } } if (result) { callback->OnLookupComplete(this, result, status); } return rv; } void nsHostResolver::DetachCallback(const char *host, uint16_t flags, uint16_t af, const char *netInterface, nsResolveHostCallback *callback, nsresult status) { RefPtr rec; { MutexAutoLock lock(mLock); nsHostKey key = { host, flags, af, netInterface }; auto he = static_cast(mDB.Search(&key)); if (he) { // walk list looking for |callback|... we cannot assume // that it will be there! PRCList *node = he->rec->callbacks.next; while (node != &he->rec->callbacks) { if (static_cast(node) == callback) { PR_REMOVE_LINK(callback); rec = he->rec; break; } node = node->next; } } } // 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->OnLookupComplete(this, rec, status); } nsresult nsHostResolver::ConditionallyCreateThread(nsHostRecord *rec) { if (mNumIdleThreads) { // wake up idle thread to process this lookup mIdleThreadCV.Notify(); } else if ((mThreadCount < HighThreadThreshold) || (IsHighPriority(rec->flags) && mThreadCount < MAX_RESOLVER_THREADS)) { // dispatch new worker thread NS_ADDREF_THIS(); // owning reference passed to thread mThreadCount++; PRThread *thr = PR_CreateThread(PR_SYSTEM_THREAD, ThreadFunc, this, PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD, 0); if (!thr) { mThreadCount--; NS_RELEASE_THIS(); return NS_ERROR_OUT_OF_MEMORY; } } else { LOG((" Unable to find a thread for looking up host [%s%s%s].\n", LOG_HOST(rec->host, rec->netInterface))); } return NS_OK; } nsresult nsHostResolver::IssueLookup(nsHostRecord *rec) { nsresult rv = NS_OK; NS_ASSERTION(!rec->resolving, "record is already being resolved"); // Add rec to one of the pending queues, possibly removing it from mEvictionQ. // If rec is on mEvictionQ, then we can just move the owning // reference over to the new active queue. if (rec->next == rec) NS_ADDREF(rec); else { PR_REMOVE_LINK(rec); mEvictionQSize--; } switch (nsHostRecord::GetPriority(rec->flags)) { case nsHostRecord::DNS_PRIORITY_HIGH: PR_APPEND_LINK(rec, &mHighQ); break; case nsHostRecord::DNS_PRIORITY_MEDIUM: PR_APPEND_LINK(rec, &mMediumQ); break; case nsHostRecord::DNS_PRIORITY_LOW: PR_APPEND_LINK(rec, &mLowQ); break; } mPendingCount++; rec->resolving = true; rec->onQueue = true; rv = ConditionallyCreateThread(rec); LOG ((" DNS thread counters: total=%d any-live=%d idle=%d pending=%d\n", static_cast(mThreadCount), static_cast(mActiveAnyThreadCount), static_cast(mNumIdleThreads), static_cast(mPendingCount))); return rv; } nsresult nsHostResolver::ConditionallyRefreshRecord(nsHostRecord *rec, const char *host) { if ((rec->CheckExpiration(TimeStamp::NowLoRes()) != nsHostRecord::EXP_VALID || rec->negative) && !rec->resolving) { LOG((" Using %s cache entry for host [%s] but starting async renewal.", rec->negative ? "negative" :"positive", host)); IssueLookup(rec); if (!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; } void nsHostResolver::DeQueue(PRCList &aQ, nsHostRecord **aResult) { *aResult = static_cast(aQ.next); PR_REMOVE_AND_INIT_LINK(*aResult); mPendingCount--; (*aResult)->onQueue = false; } bool nsHostResolver::GetHostToLookup(nsHostRecord **result) { bool timedOut = false; PRIntervalTime epoch, now, timeout; MutexAutoLock lock(mLock); timeout = (mNumIdleThreads >= HighThreadThreshold) ? mShortIdleTimeout : mLongIdleTimeout; epoch = PR_IntervalNow(); while (!mShutdown) { // remove next record from Q; hand over owning reference. Check high, then med, then low #if TTL_AVAILABLE #define SET_GET_TTL(var, val) \ (var)->mGetTtl = sGetTtlEnabled && (val) #else #define SET_GET_TTL(var, val) #endif if (!PR_CLIST_IS_EMPTY(&mHighQ)) { DeQueue (mHighQ, result); SET_GET_TTL(*result, false); return true; } if (mActiveAnyThreadCount < HighThreadThreshold) { if (!PR_CLIST_IS_EMPTY(&mMediumQ)) { DeQueue (mMediumQ, result); mActiveAnyThreadCount++; (*result)->usingAnyThread = true; SET_GET_TTL(*result, true); return true; } if (!PR_CLIST_IS_EMPTY(&mLowQ)) { DeQueue (mLowQ, result); mActiveAnyThreadCount++; (*result)->usingAnyThread = true; SET_GET_TTL(*result, true); 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 mNumIdleThreads++; mIdleThreadCV.Wait(timeout); mNumIdleThreads--; now = PR_IntervalNow(); if ((PRIntervalTime)(now - epoch) >= timeout) timedOut = true; else { // It is possible that PR_WaitCondVar() 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 -= (PRIntervalTime)(now - epoch); epoch = now; } } // tell thread to exit... return false; } void nsHostResolver::PrepareRecordExpiration(nsHostRecord* rec) const { MOZ_ASSERT(((bool)rec->addr_info) != rec->negative); if (!rec->addr_info) { rec->SetExpiration(TimeStamp::NowLoRes(), NEGATIVE_RECORD_LIFETIME, 0); LOG(("Caching host [%s%s%s] negative record for %u seconds.\n", LOG_HOST(rec->host, rec->netInterface), NEGATIVE_RECORD_LIFETIME)); return; } unsigned int lifetime = mDefaultCacheLifetime; unsigned int grace = mDefaultGracePeriod; #if TTL_AVAILABLE unsigned int ttl = mDefaultCacheLifetime; if (sGetTtlEnabled) { MutexAutoLock lock(rec->addr_info_lock); if (rec->addr_info && rec->addr_info->ttl != AddrInfo::NO_TTL_DATA) { ttl = rec->addr_info->ttl; } lifetime = ttl; grace = 0; } #endif rec->SetExpiration(TimeStamp::NowLoRes(), lifetime, grace); LOG(("Caching host [%s%s%s] record for %u seconds (grace %d).", LOG_HOST(rec->host, rec->netInterface), lifetime, grace)); } // // OnLookupComplete() 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::OnLookupComplete(nsHostRecord* rec, nsresult status, AddrInfo* result) { // get the list of pending callbacks for this lookup, and notify // them that the lookup is complete. PRCList cbs; PR_INIT_CLIST(&cbs); { MutexAutoLock lock(mLock); if (rec->mResolveAgain && (status != NS_ERROR_ABORT)) { rec->mResolveAgain = false; return LOOKUP_RESOLVEAGAIN; } // grab list of callbacks to notify MoveCList(rec->callbacks, cbs); // update record fields. We might have a rec->addr_info already if a // previous lookup result expired and we're reresolving it.. AddrInfo *old_addr_info; { MutexAutoLock lock(rec->addr_info_lock); old_addr_info = rec->addr_info; rec->addr_info = result; rec->addr_info_gencnt++; } delete old_addr_info; rec->negative = !rec->addr_info; PrepareRecordExpiration(rec); rec->resolving = false; if (rec->usingAnyThread) { mActiveAnyThreadCount--; rec->usingAnyThread = false; } if (!mShutdown) { // add to mEvictionQ PR_APPEND_LINK(rec, &mEvictionQ); NS_ADDREF(rec); if (mEvictionQSize < mMaxCacheEntries) mEvictionQSize++; else { // remove first element on mEvictionQ nsHostRecord *head = static_cast(PR_LIST_HEAD(&mEvictionQ)); PR_REMOVE_AND_INIT_LINK(head); mDB.Remove((nsHostKey *) head); if (!head->negative) { // record the age of the entry upon eviction. TimeDuration age = TimeStamp::NowLoRes() - head->mValidStart; Telemetry::Accumulate(Telemetry::DNS_CLEANUP_AGE, static_cast(age.ToSeconds() / 60)); } // release reference to rec owned by mEvictionQ NS_RELEASE(head); } #if TTL_AVAILABLE if (!rec->mGetTtl && !rec->resolving && sGetTtlEnabled) { LOG(("Issuing second async lookup for TTL for host [%s%s%s].", LOG_HOST(rec->host, rec->netInterface))); rec->flags = (rec->flags & ~RES_PRIORITY_MEDIUM) | RES_PRIORITY_LOW; DebugOnly rv = IssueLookup(rec); NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Could not issue second async lookup for TTL."); } #endif } } if (!PR_CLIST_IS_EMPTY(&cbs)) { PRCList *node = cbs.next; while (node != &cbs) { nsResolveHostCallback *callback = static_cast(node); node = node->next; callback->OnLookupComplete(this, rec, status); // NOTE: callback must not be dereferenced after this point!! } } NS_RELEASE(rec); return LOOKUP_OK; } void nsHostResolver::CancelAsyncRequest(const char *host, uint16_t flags, uint16_t af, const char *netInterface, nsIDNSListener *aListener, nsresult status) { MutexAutoLock lock(mLock); // Lookup the host record associated with host, flags & address family nsHostKey key = { host, flags, af, netInterface }; auto he = static_cast(mDB.Search(&key)); if (he) { nsHostRecord* recPtr = nullptr; PRCList *node = he->rec->callbacks.next; // Remove the first nsDNSAsyncRequest callback which matches the // supplied listener object while (node != &he->rec->callbacks) { nsResolveHostCallback *callback = static_cast(node); if (callback && (callback->EqualsAsyncListener(aListener))) { // Remove from the list of callbacks PR_REMOVE_LINK(callback); recPtr = he->rec; callback->OnLookupComplete(this, recPtr, status); break; } node = node->next; } // If there are no more callbacks, remove the hash table entry if (recPtr && PR_CLIST_IS_EMPTY(&recPtr->callbacks)) { mDB.Remove((nsHostKey *)recPtr); // If record is on a Queue, remove it and then deref it if (recPtr->next != recPtr) { PR_REMOVE_LINK(recPtr); NS_RELEASE(recPtr); } } } } size_t nsHostResolver::SizeOfIncludingThis(MallocSizeOf mallocSizeOf) const { MutexAutoLock lock(mLock); size_t n = mallocSizeOf(this); n += mDB.ShallowSizeOfExcludingThis(mallocSizeOf); for (auto iter = mDB.ConstIter(); !iter.Done(); iter.Next()) { auto entry = static_cast(iter.Get()); n += entry->rec->SizeOfIncludingThis(mallocSizeOf); } // The following fields aren't measured. // - mHighQ, mMediumQ, mLowQ, mEvictionQ, because they just point to // nsHostRecords that also pointed to by entries |mDB|, and measured when // |mDB| is measured. return n; } void nsHostResolver::ThreadFunc(void *arg) { LOG(("DNS lookup thread - starting execution.\n")); static nsThreadPoolNaming naming; naming.SetThreadPoolName(NS_LITERAL_CSTRING("DNS Resolver")); #if defined(RES_RETRY_ON_FAILURE) nsResState rs; #endif nsHostResolver *resolver = (nsHostResolver *)arg; nsHostRecord *rec = nullptr; AddrInfo *ai = nullptr; while (rec || resolver->GetHostToLookup(&rec)) { LOG(("DNS lookup thread - Calling getaddrinfo for host [%s%s%s].\n", LOG_HOST(rec->host, rec->netInterface))); TimeStamp startTime = TimeStamp::Now(); #if TTL_AVAILABLE bool getTtl = rec->mGetTtl; #else bool getTtl = false; #endif nsresult status = GetAddrInfo(rec->host, rec->af, rec->flags, rec->netInterface, &ai, getTtl); #if defined(RES_RETRY_ON_FAILURE) if (NS_FAILED(status) && rs.Reset()) { status = GetAddrInfo(rec->host, rec->af, rec->flags, rec->netInterface, &ai, getTtl); } #endif { // obtain lock to check shutdown and manage inter-module telemetry MutexAutoLock lock(resolver->mLock); if (!resolver->mShutdown) { TimeDuration elapsed = TimeStamp::Now() - startTime; uint32_t millis = static_cast(elapsed.ToMilliseconds()); if (NS_SUCCEEDED(status)) { Telemetry::ID 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); } } } // OnLookupComplete may release "rec", long before we lose it. LOG(("DNS lookup thread - lookup completed for host [%s%s%s]: %s.\n", LOG_HOST(rec->host, rec->netInterface), ai ? "success" : "failure: unknown host")); if (LOOKUP_RESOLVEAGAIN == resolver->OnLookupComplete(rec, status, ai)) { // leave 'rec' assigned and loop to make a renewed host resolve LOG(("DNS lookup thread - Re-resolving host [%s%s%s].\n", LOG_HOST(rec->host, rec->netInterface))); } else { rec = nullptr; } } resolver->mThreadCount--; NS_RELEASE(resolver); LOG(("DNS lookup thread - queue empty, thread finished.\n")); } nsresult nsHostResolver::Create(uint32_t maxCacheEntries, uint32_t defaultCacheEntryLifetime, uint32_t defaultGracePeriod, nsHostResolver **result) { nsHostResolver *res = new nsHostResolver(maxCacheEntries, defaultCacheEntryLifetime, defaultGracePeriod); NS_ADDREF(res); nsresult rv = res->Init(); if (NS_FAILED(rv)) NS_RELEASE(res); *result = res; return rv; } void nsHostResolver::GetDNSCacheEntries(nsTArray *args) { for (auto iter = mDB.Iter(); !iter.Done(); iter.Next()) { // We don't pay attention to address literals, only resolved domains. // Also require a host. auto entry = static_cast(iter.Get()); nsHostRecord* rec = entry->rec; MOZ_ASSERT(rec, "rec should never be null here!"); if (!rec || !rec->addr_info || !rec->host) { continue; } DNSCacheEntries info; info.hostname = rec->host; info.family = rec->af; info.netInterface = rec->netInterface; info.expiration = (int64_t)(rec->mValidEnd - TimeStamp::NowLoRes()).ToSeconds(); if (info.expiration <= 0) { // We only need valid DNS cache entries continue; } { MutexAutoLock lock(rec->addr_info_lock); NetAddr *addr = nullptr; NetAddrElement *addrElement = rec->addr_info->mAddresses.getFirst(); if (addrElement) { addr = &addrElement->mAddress; } while (addr) { char buf[kIPv6CStrBufSize]; if (NetAddrToString(addr, buf, sizeof(buf))) { info.hostaddr.AppendElement(buf); } addr = nullptr; addrElement = addrElement->getNext(); if (addrElement) { addr = &addrElement->mAddress; } } } args->AppendElement(info); } }