/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set et sw=2 ts=4: */ /* 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/. */ // We define this to make our use of inet_ntoa() pass. The "proper" function // inet_ntop() doesn't exist on Windows XP. #define _WINSOCK_DEPRECATED_NO_WARNINGS #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mozilla/Logging.h" #include "nsComponentManagerUtils.h" #include "nsThreadUtils.h" #include "nsIObserverService.h" #include "nsIWindowsRegKey.h" #include "nsServiceManagerUtils.h" #include "nsNetAddr.h" #include "nsNotifyAddrListener.h" #include "nsString.h" #include "nsPrintfCString.h" #include "mozilla/Services.h" #include "nsCRT.h" #include "nsThreadPool.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/SHA1.h" #include "mozilla/Base64.h" #include "mozilla/ScopeExit.h" #include "mozilla/Telemetry.h" #include "../LinkServiceCommon.h" #include #include using namespace mozilla; static LazyLogModule gNotifyAddrLog("nsNotifyAddr"); #define LOG(args) MOZ_LOG(gNotifyAddrLog, mozilla::LogLevel::Debug, args) #define LOG_ENABLED() MOZ_LOG_TEST(gNotifyAddrLog, mozilla::LogLevel::Debug) // period during which to absorb subsequent network change events, in // milliseconds static const unsigned int kNetworkChangeCoalescingPeriod = 1000; NS_IMPL_ISUPPORTS(nsNotifyAddrListener, nsINetworkLinkService, nsIRunnable, nsIObserver) nsNotifyAddrListener::nsNotifyAddrListener() : mLinkUp(true), // assume true by default mStatusKnown(false), mCheckAttempted(false), mMutex("nsNotifyAddrListener::mMutex"), mCheckEvent(nullptr), mShutdown(false), mPlatformDNSIndications(NONE_DETECTED), mIPInterfaceChecksum(0), mCoalescingActive(false) {} nsNotifyAddrListener::~nsNotifyAddrListener() { NS_ASSERTION(!mThread, "nsNotifyAddrListener thread shutdown failed"); } NS_IMETHODIMP nsNotifyAddrListener::GetIsLinkUp(bool* aIsUp) { if (!mCheckAttempted && !mStatusKnown) { mCheckAttempted = true; CheckLinkStatus(); } *aIsUp = mLinkUp; return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetLinkStatusKnown(bool* aIsUp) { *aIsUp = mStatusKnown; return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetLinkType(uint32_t* aLinkType) { NS_ENSURE_ARG_POINTER(aLinkType); // XXX This function has not yet been implemented for this platform *aLinkType = nsINetworkLinkService::LINK_TYPE_UNKNOWN; return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetNetworkID(nsACString& aNetworkID) { MutexAutoLock lock(mMutex); aNetworkID = mNetworkId; return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetDnsSuffixList(nsTArray& aDnsSuffixList) { aDnsSuffixList.Clear(); MutexAutoLock lock(mMutex); aDnsSuffixList.AppendElements(mDnsSuffixList); return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetResolvers(nsTArray>& aResolvers) { nsTArray addresses; nsresult rv = GetNativeResolvers(addresses); if (NS_FAILED(rv)) { return rv; } for (const auto& addr : addresses) { aResolvers.AppendElement(MakeRefPtr(&addr)); } return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetNativeResolvers( nsTArray& aResolvers) { aResolvers.Clear(); MutexAutoLock lock(mMutex); aResolvers.AppendElements(mDNSResolvers); return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::GetPlatformDNSIndications( uint32_t* aPlatformDNSIndications) { *aPlatformDNSIndications = mPlatformDNSIndications; return NS_OK; } // // Hash the sorted network ids // void nsNotifyAddrListener::HashSortedNetworkIds(std::vector nwGUIDS, SHA1Sum& sha1) { std::sort(nwGUIDS.begin(), nwGUIDS.end(), [](const GUID& a, const GUID& b) { return memcmp(&a, &b, sizeof(GUID)) < 0; }); for (auto const& nwGUID : nwGUIDS) { sha1.update(&nwGUID, sizeof(GUID)); if (LOG_ENABLED()) { nsPrintfCString guid("%08lX%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X", nwGUID.Data1, nwGUID.Data2, nwGUID.Data3, nwGUID.Data4[0], nwGUID.Data4[1], nwGUID.Data4[2], nwGUID.Data4[3], nwGUID.Data4[4], nwGUID.Data4[5], nwGUID.Data4[6], nwGUID.Data4[7]); LOG(("calculateNetworkId: interface networkID: %s\n", guid.get())); } } } // // Figure out the current "network identification" string. // void nsNotifyAddrListener::calculateNetworkId(void) { MOZ_ASSERT(!NS_IsMainThread(), "Must not be called on the main thread"); // No need to recompute the networkId if we're shutting down. if (mShutdown) { return; } if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { return; } auto unitialize = MakeScopeExit([]() { CoUninitialize(); }); RefPtr nlm; HRESULT hr = CoCreateInstance(CLSID_NetworkListManager, nullptr, CLSCTX_ALL, IID_INetworkListManager, getter_AddRefs(nlm)); if (NS_WARN_IF(FAILED(hr))) { LOG(("CoCreateInstance error: %lX", hr)); return; } RefPtr enumNetworks; hr = nlm->GetNetworks(NLM_ENUM_NETWORK_CONNECTED, getter_AddRefs(enumNetworks)); if (NS_WARN_IF(FAILED(hr))) { LOG(("GetNetworks error: %lX", hr)); return; } // We will hash the found network ids // for privacy reasons SHA1Sum sha1; // The networks stored in enumNetworks // are not ordered. We will sort them // To keep a consistent hash // regardless of the found networks order. std::vector nwGUIDS; // Consume the found networks iterator while (true) { RefPtr network; hr = enumNetworks->Next(1, getter_AddRefs(network), nullptr); if (hr != S_OK) { break; } GUID nwGUID; hr = network->GetNetworkId(&nwGUID); if (hr != S_OK) { continue; } nwGUIDS.push_back(nwGUID); } if (nwGUIDS.empty()) { bool idChanged = false; { MutexAutoLock lock(mMutex); if (!mNetworkId.IsEmpty()) { idChanged = true; } mNetworkId.Truncate(); } LOG(("calculateNetworkId: no network ID - no active networks")); Telemetry::Accumulate(Telemetry::NETWORK_ID2, 0); if (idChanged) { NotifyObservers(NS_NETWORK_ID_CHANGED_TOPIC, nullptr); } return; } nsAutoCString output; SHA1Sum::Hash digest; HashSortedNetworkIds(nwGUIDS, sha1); SeedNetworkId(sha1); sha1.finish(digest); nsCString newString(reinterpret_cast(digest), SHA1Sum::kHashSize); nsresult rv = Base64Encode(newString, output); if (NS_FAILED(rv)) { { MutexAutoLock lock(mMutex); mNetworkId.Truncate(); } Telemetry::Accumulate(Telemetry::NETWORK_ID2, 0); LOG(("calculateNetworkId: no network ID Base64Encode error %X", uint32_t(rv))); return; } MutexAutoLock lock(mMutex); if (output != mNetworkId) { mNetworkId = output; Telemetry::Accumulate(Telemetry::NETWORK_ID2, 1); LOG(("calculateNetworkId: new NetworkID: %s", output.get())); NotifyObservers(NS_NETWORK_ID_CHANGED_TOPIC, nullptr); } else { Telemetry::Accumulate(Telemetry::NETWORK_ID2, 2); LOG(("calculateNetworkId: same NetworkID: %s", output.get())); } } // Static Callback function for NotifyIpInterfaceChange API. static void WINAPI OnInterfaceChange(PVOID callerContext, PMIB_IPINTERFACE_ROW row, MIB_NOTIFICATION_TYPE notificationType) { nsNotifyAddrListener* notify = static_cast(callerContext); notify->CheckLinkStatus(); } DWORD nsNotifyAddrListener::nextCoalesceWaitTime() { // check if coalescing period should continue double period = (TimeStamp::Now() - mChangeTime).ToMilliseconds(); if (period >= kNetworkChangeCoalescingPeriod) { NotifyObservers(NS_NETWORK_LINK_TOPIC, NS_NETWORK_LINK_DATA_CHANGED); mCoalescingActive = false; return INFINITE; // return default } else { // wait no longer than to the end of the period return static_cast(kNetworkChangeCoalescingPeriod - period); } } NS_IMETHODIMP nsNotifyAddrListener::Run() { mStartTime = TimeStamp::Now(); calculateNetworkId(); DWORD waitTime = INFINITE; // Windows Vista and newer versions. HANDLE interfacechange; // The callback will simply invoke CheckLinkStatus() DWORD ret = NotifyIpInterfaceChange( StaticPrefs::network_notify_IPv6() ? AF_UNSPEC : AF_INET, // IPv4 and IPv6 (PIPINTERFACE_CHANGE_CALLBACK)OnInterfaceChange, this, // pass to callback StaticPrefs::network_notify_initial_call(), // initial notification &interfacechange); if (ret == NO_ERROR) { do { ret = WaitForSingleObject(mCheckEvent, waitTime); if (!mShutdown) { waitTime = nextCoalesceWaitTime(); } else { break; } } while (ret != WAIT_FAILED); CancelMibChangeNotify2(interfacechange); } else { LOG(("Link Monitor: NotifyIpInterfaceChange returned %d\n", (int)ret)); } return NS_OK; } NS_IMETHODIMP nsNotifyAddrListener::Observe(nsISupports* subject, const char* topic, const char16_t* data) { if (!strcmp("xpcom-shutdown-threads", topic)) Shutdown(); return NS_OK; } nsresult nsNotifyAddrListener::Init(void) { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (!observerService) return NS_ERROR_FAILURE; nsresult rv = observerService->AddObserver(this, "xpcom-shutdown-threads", false); NS_ENSURE_SUCCESS(rv, rv); mCheckEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr); NS_ENSURE_TRUE(mCheckEvent, NS_ERROR_OUT_OF_MEMORY); nsCOMPtr threadPool = new nsThreadPool(); MOZ_ALWAYS_SUCCEEDS(threadPool->SetThreadLimit(1)); MOZ_ALWAYS_SUCCEEDS( threadPool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize)); MOZ_ALWAYS_SUCCEEDS(threadPool->SetName("Link Monitor"_ns)); mThread = threadPool.forget(); return mThread->Dispatch(this, NS_DISPATCH_NORMAL); } nsresult nsNotifyAddrListener::Shutdown(void) { // remove xpcom shutdown observer nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) observerService->RemoveObserver(this, "xpcom-shutdown-threads"); if (!mCheckEvent) return NS_OK; mShutdown = true; SetEvent(mCheckEvent); nsresult rv = mThread ? mThread->ShutdownWithTimeout(2000) : NS_OK; // Have to break the cycle here, otherwise nsNotifyAddrListener holds // onto the thread and the thread holds onto the nsNotifyAddrListener // via its mRunnable mThread = nullptr; CloseHandle(mCheckEvent); mCheckEvent = nullptr; return rv; } /* * A network event has been registered. Delay the actual sending of the event * for a while and absorb subsequent events in the mean time in an effort to * squash potentially many triggers into a single event. * Only ever called from the same thread. */ nsresult nsNotifyAddrListener::NetworkChanged() { if (mCoalescingActive) { LOG(("NetworkChanged: absorbed an event (coalescing active)\n")); } else { // A fresh trigger! mChangeTime = TimeStamp::Now(); mCoalescingActive = true; SetEvent(mCheckEvent); LOG(("NetworkChanged: coalescing period started\n")); } return NS_OK; } /* Sends the given event. Assumes aTopic/aData never goes out of scope (static * strings are ideal). */ nsresult nsNotifyAddrListener::NotifyObservers(const char* aTopic, const char* aData) { LOG(("NotifyObservers: %s=%s\n", aTopic, aData)); if (mShutdown) { LOG(("NotifyObservers call failed when called during shutdown")); return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } auto runnable = [self = RefPtr(this), aTopic, aData] { nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) observerService->NotifyObservers( static_cast(self.get()), aTopic, !aData ? nullptr : NS_ConvertASCIItoUTF16(aData).get()); }; nsresult rv = NS_DispatchToMainThread(NS_NewRunnableFunction( "nsNotifyAddrListener::NotifyObservers", runnable)); if (NS_FAILED(rv)) { NS_WARNING( "nsNotifyAddrListener::NotifyObservers Failed to dispatch observer " "notification"); } return rv; } DWORD nsNotifyAddrListener::CheckAdaptersAddresses(void) { MOZ_ASSERT(!NS_IsMainThread(), "Don't call this on the main thread"); ULONG len = 16384; PIP_ADAPTER_ADDRESSES adapterList = (PIP_ADAPTER_ADDRESSES)moz_xmalloc(len); ULONG flags = GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST; if (!StaticPrefs::network_notify_resolvers()) { flags |= GAA_FLAG_SKIP_DNS_SERVER; } DWORD ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapterList, &len); if (ret == ERROR_BUFFER_OVERFLOW) { free(adapterList); adapterList = static_cast(moz_xmalloc(len)); ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapterList, &len); } if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) { free(adapterList); return ERROR_NOT_SUPPORTED; } // // Since NotifyIpInterfaceChange() signals a change more often than we // think is a worthy change, we checksum the entire state of all interfaces // that are UP. If the checksum is the same as previous check, nothing // of interest changed! // ULONG sumAll = 0; nsTArray dnsSuffixList; nsTArray resolvers; uint32_t platformDNSIndications = NONE_DETECTED; if (ret == ERROR_SUCCESS) { bool linkUp = false; ULONG sum = 0; for (PIP_ADAPTER_ADDRESSES adapter = adapterList; adapter; adapter = adapter->Next) { if (adapter->OperStatus != IfOperStatusUp || !adapter->FirstUnicastAddress || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) { continue; } LOG(("Adapter %s type: %lu", NS_ConvertUTF16toUTF8(adapter->FriendlyName).get(), adapter->IfType)); if (adapter->IfType == IF_TYPE_PPP || adapter->IfType == IF_TYPE_PROP_VIRTUAL || nsDependentString(adapter->FriendlyName).Find(u"VPN") != kNotFound || nsDependentString(adapter->Description).Find(u"VPN") != kNotFound) { LOG(("VPN connection found")); platformDNSIndications |= VPN_DETECTED; } sum <<= 2; // Add chars from AdapterName to the checksum. for (int i = 0; adapter->AdapterName[i]; ++i) { sum += adapter->AdapterName[i]; } // Add bytes from each socket address to the checksum. for (PIP_ADAPTER_UNICAST_ADDRESS pip = adapter->FirstUnicastAddress; pip; pip = pip->Next) { SOCKET_ADDRESS* sockAddr = &pip->Address; for (int i = 0; i < sockAddr->iSockaddrLength; ++i) { sum += (reinterpret_cast(sockAddr->lpSockaddr))[i]; } } for (IP_ADAPTER_DNS_SERVER_ADDRESS* pDnServer = adapter->FirstDnsServerAddress; pDnServer; pDnServer = pDnServer->Next) { mozilla::net::NetAddr addr; if (pDnServer->Address.lpSockaddr->sa_family == AF_INET) { const struct sockaddr_in* sin = (const struct sockaddr_in*)pDnServer->Address.lpSockaddr; addr.inet.family = AF_INET; addr.inet.ip = sin->sin_addr.s_addr; addr.inet.port = sin->sin_port; } else if (pDnServer->Address.lpSockaddr->sa_family == AF_INET6) { const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)pDnServer->Address.lpSockaddr; addr.inet6.family = AF_INET6; memcpy(&addr.inet6.ip.u8, &sin6->sin6_addr, sizeof(addr.inet6.ip.u8)); addr.inet6.port = sin6->sin6_port; } else { NS_WARNING("Unexpected addr type"); continue; } if (LOG_ENABLED()) { char buf[100]; addr.ToStringBuffer(buf, 100); LOG(("Found DNS resolver=%s", buf)); } resolvers.AppendElement(addr); } if (StaticPrefs::network_notify_dnsSuffixList()) { nsCString suffix = NS_ConvertUTF16toUTF8(adapter->DnsSuffix); if (!suffix.IsEmpty()) { LOG((" found DNS suffix=%s\n", suffix.get())); dnsSuffixList.AppendElement(suffix); } } linkUp = true; sumAll ^= sum; } mLinkUp = linkUp; mStatusKnown = true; } free(adapterList); if (mLinkUp) { /* Store the checksum only if one or more interfaces are up */ mIPInterfaceChecksum = sumAll; } CoUninitialize(); if (StaticPrefs::network_notify_dnsSuffixList()) { // It seems that the only way to retrieve non-connection specific DNS // suffixes is via the Windows registry. // This function takes a registry path. If aRegPath\\SearchList is // found and successfully parsed, then it returns true. Otherwise it // returns false. auto checkRegistry = [&dnsSuffixList](const nsAString& aRegPath) -> bool { nsresult rv; nsCOMPtr regKey = do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); if (NS_FAILED(rv)) { LOG((" creating nsIWindowsRegKey failed\n")); return false; } rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, aRegPath, nsIWindowsRegKey::ACCESS_READ); if (NS_FAILED(rv)) { LOG((" opening registry key failed\n")); return false; } nsAutoString wideSuffixString; rv = regKey->ReadStringValue(u"SearchList"_ns, wideSuffixString); if (NS_FAILED(rv)) { LOG((" reading registry string value failed\n")); return false; } // Normally the key should not contain whitespace, but editing the // registry manually or through gpedit doesn't alway enforce this. nsAutoCString list = NS_ConvertUTF16toUTF8(wideSuffixString); list.StripWhitespace(); for (const nsACString& suffix : list.Split(',')) { LOG((" appending DNS suffix from registry: %s\n", suffix.BeginReading())); if (!suffix.IsEmpty()) { dnsSuffixList.AppendElement(suffix); } } return true; }; // The Local group policy overrides the user set suffix list, so we must // first check the registry key that is sets by gpedit, and if that fails we // fall back to the one that is set by the user. if (!checkRegistry(nsLiteralString( u"SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient"))) { checkRegistry(nsLiteralString( u"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters")); } } auto registryChildCount = [](const nsAString& aRegPath) -> uint32_t { nsresult rv; nsCOMPtr regKey = do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); if (NS_FAILED(rv)) { LOG((" creating nsIWindowsRegKey failed\n")); return 0; } rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, aRegPath, nsIWindowsRegKey::ACCESS_READ); if (NS_FAILED(rv)) { LOG((" opening registry key failed\n")); return 0; } uint32_t count = 0; rv = regKey->GetChildCount(&count); if (NS_FAILED(rv)) { return 0; } return count; }; if (StaticPrefs::network_notify_checkForProxies()) { if (registryChildCount(u"SYSTEM\\CurrentControlSet\\Services\\Dnscache\\" "Parameters\\DnsConnections"_ns) > 0 || registryChildCount(u"SYSTEM\\CurrentControlSet\\Services\\Dnscache\\" "Parameters\\DnsConnectionsProxies"_ns) > 0) { platformDNSIndications |= PROXY_DETECTED; } } if (StaticPrefs::network_notify_checkForNRPT()) { if (registryChildCount(u"SYSTEM\\CurrentControlSet\\Services\\Dnscache\\" "Parameters\\DnsPolicyConfig"_ns) > 0 || registryChildCount(u"SOFTWARE\\Policies\\Microsoft\\Windows NT\\" "DNSClient\\DnsPolicyConfig"_ns) > 0) { platformDNSIndications |= NRPT_DETECTED; } } { MutexAutoLock lock(mMutex); mDnsSuffixList = std::move(dnsSuffixList); mDNSResolvers = std::move(resolvers); mPlatformDNSIndications = platformDNSIndications; } NotifyObservers(NS_DNS_SUFFIX_LIST_UPDATED_TOPIC, nullptr); calculateNetworkId(); return ret; } /** * Checks the status of all network adapters. If one is up and has a valid IP * address, sets mLinkUp to true. Sets mStatusKnown to true if the link status * is definitive. */ void nsNotifyAddrListener::CheckLinkStatus(void) { DWORD ret; const char* event; bool prevLinkUp = mLinkUp; ULONG prevCsum = mIPInterfaceChecksum; LOG(("check status of all network adapters\n")); // The CheckAdaptersAddresses call is very expensive (~650 milliseconds), // so we don't want to call it synchronously. Instead, we just start up // assuming we have a network link, but we'll report that the status is // unknown. if (NS_IsMainThread()) { NS_WARNING( "CheckLinkStatus called on main thread! No check " "performed. Assuming link is up, status is unknown."); mLinkUp = true; if (!mStatusKnown) { event = NS_NETWORK_LINK_DATA_UNKNOWN; } else if (!prevLinkUp) { event = NS_NETWORK_LINK_DATA_UP; } else { // Known status and it was already UP event = nullptr; } if (event) { NotifyObservers(NS_NETWORK_LINK_TOPIC, event); } } else { ret = CheckAdaptersAddresses(); if (ret != ERROR_SUCCESS) { mLinkUp = true; } if (mLinkUp && (prevCsum != mIPInterfaceChecksum)) { TimeDuration since = TimeStamp::Now() - mStartTime; // Network is online. Topology has changed. Always send CHANGED // before UP - if allowed to and having cooled down. if (StaticPrefs::network_notify_changed() && (since.ToMilliseconds() > 2000)) { NetworkChanged(); } } if (prevLinkUp != mLinkUp) { // UP/DOWN status changed, send appropriate UP/DOWN event NotifyObservers(NS_NETWORK_LINK_TOPIC, mLinkUp ? NS_NETWORK_LINK_DATA_UP : NS_NETWORK_LINK_DATA_DOWN); } } }