diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /netwerk/system/mac/nsNetworkLinkService.mm | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/system/mac/nsNetworkLinkService.mm')
-rw-r--r-- | netwerk/system/mac/nsNetworkLinkService.mm | 947 |
1 files changed, 947 insertions, 0 deletions
diff --git a/netwerk/system/mac/nsNetworkLinkService.mm b/netwerk/system/mac/nsNetworkLinkService.mm new file mode 100644 index 0000000000..8f7b4ff767 --- /dev/null +++ b/netwerk/system/mac/nsNetworkLinkService.mm @@ -0,0 +1,947 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ +#include <numeric> +#include <vector> +#include <algorithm> + +#include <sys/socket.h> +#include <sys/sysctl.h> + +#include <net/if.h> +#include <net/if_dl.h> +#include <net/if_types.h> +#include <net/route.h> +#include <netinet/in.h> +#include <netinet/if_ether.h> +#include <arpa/inet.h> +#include <ifaddrs.h> +#include <resolv.h> + +#include "nsCOMPtr.h" +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsCRT.h" +#include "nsNetCID.h" +#include "nsThreadUtils.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/SHA1.h" +#include "mozilla/Base64.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Services.h" +#include "mozilla/Telemetry.h" +#include "nsNetworkLinkService.h" +#include "../../base/IPv6Utils.h" +#include "../LinkServiceCommon.h" +#include "../NetworkLinkServiceDefines.h" + +#import <Cocoa/Cocoa.h> +#import <netinet/in.h> + +#define NETWORK_NOTIFY_CHANGED_PREF "network.notify.changed" + +using namespace mozilla; + +static LazyLogModule gNotifyAddrLog("nsNotifyAddr"); +#define LOG(args) MOZ_LOG(gNotifyAddrLog, mozilla::LogLevel::Debug, args) + +// See bug 1584165. Sometimes the ARP table is empty or doesn't have +// the entry of gateway after the network change, so we'd like to delay +// the calaulation of network id. +static const uint32_t kNetworkIdDelayAfterChange = 3000; + +// When you remove search domains from the settings page and hit Apply a +// network change event is generated, but res.dnsrch is not updated to the +// correct values. Thus, after a network change, we add a small delay to +// the runnable so the OS has the chance to update the values. +static const uint32_t kDNSSuffixDelayAfterChange = 50; + +// If non-successful, extract the error code and return it. This +// error code dance is inspired by +// http://developer.apple.com/technotes/tn/tn1145.html +static OSStatus getErrorCodeBool(Boolean success) { + OSStatus err = noErr; + if (!success) { + int scErr = ::SCError(); + if (scErr == kSCStatusOK) { + scErr = kSCStatusFailed; + } + err = scErr; + } + return err; +} + +// If given a NULL pointer, return the error code. +static OSStatus getErrorCodePtr(const void* value) { return getErrorCodeBool(value != nullptr); } + +// Convenience function to allow NULL input. +static void CFReleaseSafe(CFTypeRef cf) { + if (cf) { + // "If cf is NULL, this will cause a runtime error and your + // application will crash." / Apple docs + ::CFRelease(cf); + } +} + +NS_IMPL_ISUPPORTS(nsNetworkLinkService, nsINetworkLinkService, nsIObserver, nsITimerCallback, + nsINamed) + +nsNetworkLinkService::nsNetworkLinkService() + : mLinkUp(true), + mStatusKnown(false), + mReachability(nullptr), + mCFRunLoop(nullptr), + mRunLoopSource(nullptr), + mStoreRef(nullptr), + mMutex("nsNetworkLinkService::mMutex") {} + +nsNetworkLinkService::~nsNetworkLinkService() = default; + +NS_IMETHODIMP +nsNetworkLinkService::GetIsLinkUp(bool* aIsUp) { + *aIsUp = mLinkUp; + return NS_OK; +} + +NS_IMETHODIMP +nsNetworkLinkService::GetLinkStatusKnown(bool* aIsUp) { + *aIsUp = mStatusKnown; + return NS_OK; +} + +NS_IMETHODIMP +nsNetworkLinkService::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 +nsNetworkLinkService::GetNetworkID(nsACString& aNetworkID) { + MutexAutoLock lock(mMutex); + aNetworkID = mNetworkId; + return NS_OK; +} + +NS_IMETHODIMP +nsNetworkLinkService::GetPlatformDNSIndications(uint32_t* aPlatformDNSIndications) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void nsNetworkLinkService::GetDnsSuffixListInternal() { + MOZ_ASSERT(!NS_IsMainThread()); + LOG(("GetDnsSuffixListInternal")); + + auto sendNotification = mozilla::MakeScopeExit([self = RefPtr{this}] { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsNetworkLinkService::GetDnsSuffixListInternal", + [self]() { self->NotifyObservers(NS_DNS_SUFFIX_LIST_UPDATED_TOPIC, nullptr); })); + }); + + nsTArray<nsCString> result; + + struct __res_state res; + if (res_ninit(&res) == 0) { + for (int i = 0; i < MAXDNSRCH; i++) { + if (!res.dnsrch[i]) { + break; + } + LOG(("DNS search domain from [%s]\n", res.dnsrch[i])); + result.AppendElement(nsCString(res.dnsrch[i])); + } + res_nclose(&res); + } + + MutexAutoLock lock(mMutex); + mDNSSuffixList = std::move(result); +} + +NS_IMETHODIMP +nsNetworkLinkService::GetDnsSuffixList(nsTArray<nsCString>& aDnsSuffixList) { + aDnsSuffixList.Clear(); + + MutexAutoLock lock(mMutex); + aDnsSuffixList.AppendElements(mDNSSuffixList); + return NS_OK; +} + +NS_IMETHODIMP +nsNetworkLinkService::GetResolvers(nsTArray<RefPtr<nsINetAddr>>& aResolvers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsNetworkLinkService::GetNativeResolvers(nsTArray<mozilla::net::NetAddr>& aResolvers) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +#ifndef SA_SIZE +# define SA_SIZE(sa) \ + ((!(sa) || ((struct sockaddr*)(sa))->sa_len == 0) \ + ? sizeof(uint32_t) \ + : 1 + ((((struct sockaddr*)(sa))->sa_len - 1) | (sizeof(uint32_t) - 1))) +#endif + +static bool getMac(struct sockaddr_dl* sdl, char* buf, size_t bufsize) { + unsigned char* mac; + mac = (unsigned char*)LLADDR(sdl); + + if (sdl->sdl_alen != 6) { + LOG(("networkid: unexpected MAC size %u", sdl->sdl_alen)); + return false; + } + + snprintf(buf, bufsize, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], + mac[5]); + return true; +} + +/* If the IP matches, get the MAC and return true */ +static bool matchIp(struct sockaddr_dl* sdl, struct sockaddr_inarp* addr, char* ip, char* buf, + size_t bufsize) { + if (sdl->sdl_alen) { + if (!strcmp(inet_ntoa(addr->sin_addr), ip)) { + if (getMac(sdl, buf, bufsize)) { + return true; /* done! */ + } + } + } + return false; /* continue */ +} + +/* + * Scan for the 'IP' address in the ARP table and store the corresponding MAC + * address in 'mac'. The output buffer is 'maclen' bytes big. + * + * Returns 'true' if it found the IP and returns a MAC. + */ +static bool scanArp(char* ip, char* mac, size_t maclen) { + int mib[6]; + char *lim, *next; + int st; + + mib[0] = CTL_NET; + mib[1] = PF_ROUTE; + mib[2] = 0; + mib[3] = AF_INET; + mib[4] = NET_RT_FLAGS; + mib[5] = RTF_LLINFO; + + size_t needed; + if (sysctl(mib, 6, nullptr, &needed, nullptr, 0) < 0) { + return false; + } + if (needed == 0) { + LOG(("scanArp: empty table")); + return false; + } + + UniquePtr<char[]> buf(new char[needed]); + + for (;;) { + st = sysctl(mib, 6, &buf[0], &needed, nullptr, 0); + if (st == 0 || errno != ENOMEM) { + break; + } + + size_t increased = needed; + increased += increased / 8; + + auto tmp = MakeUnique<char[]>(increased); + memcpy(&tmp[0], &buf[0], needed); + buf = std::move(tmp); + needed = increased; + } + if (st == -1) { + return false; + } + lim = &buf[needed]; + + struct rt_msghdr* rtm; + for (next = &buf[0]; next < lim; next += rtm->rtm_msglen) { + rtm = reinterpret_cast<struct rt_msghdr*>(next); + struct sockaddr_inarp* sin2 = reinterpret_cast<struct sockaddr_inarp*>(rtm + 1); + struct sockaddr_dl* sdl = reinterpret_cast<struct sockaddr_dl*>((char*)sin2 + SA_SIZE(sin2)); + if (matchIp(sdl, sin2, ip, mac, maclen)) { + return true; + } + } + + return false; +} + +// Append the mac address of rtm to `stringsToHash`. If it's not in arp table, append +// ifname and IP address. +static bool parseHashKey(struct rt_msghdr* rtm, nsTArray<nsCString>& stringsToHash, + bool skipDstCheck) { + struct sockaddr* sa; + struct sockaddr_in* sockin; + char ip[INET_ADDRSTRLEN]; + + // Ignore the routing table message without destination/gateway sockaddr. + // Destination address is needed to check if the gateway is default or + // overwritten by VPN. If yes, append the mac address or IP/interface name to + // `stringsToHash`. + if ((rtm->rtm_addrs & (RTA_DST | RTA_GATEWAY)) != (RTA_DST | RTA_GATEWAY)) { + return false; + } + + sa = reinterpret_cast<struct sockaddr*>(rtm + 1); + + struct sockaddr* destination = + reinterpret_cast<struct sockaddr*>((char*)sa + RTAX_DST * SA_SIZE(sa)); + if (!destination || destination->sa_family != AF_INET) { + return false; + } + + sockin = reinterpret_cast<struct sockaddr_in*>(destination); + + inet_ntop(AF_INET, &sockin->sin_addr.s_addr, ip, sizeof(ip) - 1); + + if (!skipDstCheck && strcmp("0.0.0.0", ip)) { + return false; + } + + struct sockaddr* gateway = + reinterpret_cast<struct sockaddr*>((char*)sa + RTAX_GATEWAY * SA_SIZE(sa)); + + if (!gateway) { + return false; + } + if (gateway->sa_family == AF_INET) { + sockin = reinterpret_cast<struct sockaddr_in*>(gateway); + inet_ntop(AF_INET, &sockin->sin_addr.s_addr, ip, sizeof(ip) - 1); + char mac[18]; + + // TODO: cache the arp table instead of multiple system call. + if (scanArp(ip, mac, sizeof(mac))) { + stringsToHash.AppendElement(nsCString(mac)); + } else { + // Can't find a real MAC address. This might be a VPN gateway. + char buf[IFNAMSIZ] = {0}; + char* ifName = if_indextoname(rtm->rtm_index, buf); + if (!ifName) { + LOG(("parseHashKey: AF_INET if_indextoname failed")); + return false; + } + + stringsToHash.AppendElement(nsCString(ifName)); + stringsToHash.AppendElement(nsCString(ip)); + } + } else if (gateway->sa_family == AF_LINK) { + char buf[64]; + struct sockaddr_dl* sockdl = reinterpret_cast<struct sockaddr_dl*>(gateway); + if (getMac(sockdl, buf, sizeof(buf))) { + stringsToHash.AppendElement(nsCString(buf)); + } else { + char buf[IFNAMSIZ] = {0}; + char* ifName = if_indextoname(rtm->rtm_index, buf); + if (!ifName) { + LOG(("parseHashKey: AF_LINK if_indextoname failed")); + return false; + } + + stringsToHash.AppendElement(nsCString(ifName)); + } + } + return true; +} + +// It detects the IP of the default gateways in the routing table, then the MAC +// address of that IP in the ARP table before it hashes that string (to avoid +// information leakage). +bool nsNetworkLinkService::RoutingTable(nsTArray<nsCString>& aHash) { + size_t needed; + int mib[6]; + struct rt_msghdr* rtm; + + mib[0] = CTL_NET; + mib[1] = PF_ROUTE; + mib[2] = 0; + mib[3] = 0; + mib[4] = NET_RT_DUMP; + mib[5] = 0; + + if (sysctl(mib, 6, nullptr, &needed, nullptr, 0) < 0) { + return false; + } + + UniquePtr<char[]> buf(new char[needed]); + + if (sysctl(mib, 6, &buf[0], &needed, nullptr, 0) < 0) { + return false; + } + + char* lim = &buf[0] + needed; + bool rv = false; + + // `next + 1 < lim` ensures we have valid `rtm->rtm_msglen` which is an + // unsigned short at the beginning of `rt_msghdr`. + for (char* next = &buf[0]; next + 1 < lim; next += rtm->rtm_msglen) { + rtm = reinterpret_cast<struct rt_msghdr*>(next); + + if (next + rtm->rtm_msglen > lim) { + LOG(("Rt msg is truncated...")); + break; + } + + if (parseHashKey(rtm, aHash, false)) { + rv = true; + } + } + return rv; +} + +// Detect the routing of network.netlink.route.check.IPv4 +bool nsNetworkLinkService::RoutingFromKernel(nsTArray<nsCString>& aHash) { + int sockfd; + if ((sockfd = socket(AF_ROUTE, SOCK_RAW, 0)) == -1) { + LOG(("RoutingFromKernel: Can create a socket for network id")); + return false; + } + + MOZ_ASSERT(!NS_IsMainThread()); + + size_t needed = 1024; + struct rt_msghdr* rtm; + struct sockaddr_in* sin; + UniquePtr<char[]> buf(new char[needed]); + pid_t pid; + int seq; + + rtm = reinterpret_cast<struct rt_msghdr*>(&buf[0]); + memset(rtm, 0, sizeof(struct rt_msghdr)); + rtm->rtm_msglen = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in); + rtm->rtm_version = RTM_VERSION; + rtm->rtm_type = RTM_GET; + rtm->rtm_addrs = RTA_DST; + rtm->rtm_pid = (pid = getpid()); + rtm->rtm_seq = (seq = random()); + + sin = reinterpret_cast<struct sockaddr_in*>(rtm + 1); + memset(sin, 0, sizeof(struct sockaddr_in)); + sin->sin_len = sizeof(struct sockaddr_in); + sin->sin_family = AF_INET; + sin->sin_addr = mRouteCheckIPv4; + + if (write(sockfd, rtm, rtm->rtm_msglen) == -1) { + LOG(("RoutingFromKernel: write() failed. No route to the predefine destincation")); + return false; + } + + do { + ssize_t r; + if ((r = read(sockfd, rtm, needed)) < 0) { + LOG(("RoutingFromKernel: read() failed.")); + return false; + } + + LOG(("RoutingFromKernel: read() rtm_type: %d (%d), rtm_pid: %d (%d), rtm_seq: %d (%d)\n", + rtm->rtm_type, RTM_GET, rtm->rtm_pid, pid, rtm->rtm_seq, seq)); + } while (rtm->rtm_type != RTM_GET || rtm->rtm_pid != pid || rtm->rtm_seq != seq); + + return parseHashKey(rtm, aHash, true); +} + +// Figure out the current IPv4 "network identification" string. +bool nsNetworkLinkService::IPv4NetworkId(SHA1Sum* aSHA1) { + nsTArray<nsCString> hash; + if (!RoutingTable(hash)) { + NS_WARNING("IPv4NetworkId: No default gateways"); + } + + if (!RoutingFromKernel(hash)) { + NS_WARNING("IPv4NetworkId: No route to the predefined destination"); + } + + // We didn't get any valid hash key to generate network ID. + if (hash.IsEmpty()) { + LOG(("IPv4NetworkId: No valid hash key")); + return false; + } + + hash.Sort(); + for (uint32_t i = 0; i < hash.Length(); ++i) { + LOG(("IPv4NetworkId: Hashing string for network id: %s", hash[i].get())); + aSHA1->update(hash[i].get(), hash[i].Length()); + } + + return true; +} + +// +// Sort and hash the prefixes and netmasks +// +void nsNetworkLinkService::HashSortedPrefixesAndNetmasks( + std::vector<prefix_and_netmask> prefixAndNetmaskStore, SHA1Sum* sha1) { + // getifaddrs does not guarantee the interfaces will always be in the same order. + // We want to make sure the hash remains consistent Regardless of the interface order. + std::sort(prefixAndNetmaskStore.begin(), prefixAndNetmaskStore.end(), + [](prefix_and_netmask a, prefix_and_netmask b) { + // compare prefixStore + int comparedPrefix = memcmp(&a.first, &b.first, sizeof(in6_addr)); + if (comparedPrefix == 0) { + // compare netmaskStore + return memcmp(&a.second, &b.second, sizeof(in6_addr)) < 0; + } + return comparedPrefix < 0; + }); + + for (const auto& prefixAndNetmask : prefixAndNetmaskStore) { + sha1->update(&prefixAndNetmask.first, sizeof(in6_addr)); + sha1->update(&prefixAndNetmask.second, sizeof(in6_addr)); + } +} + +bool nsNetworkLinkService::IPv6NetworkId(SHA1Sum* sha1) { + struct ifaddrs* ifap; + std::vector<prefix_and_netmask> prefixAndNetmaskStore; + + if (!getifaddrs(&ifap)) { + struct ifaddrs* ifa; + for (ifa = ifap; ifa; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) { + continue; + } + if ((AF_INET6 == ifa->ifa_addr->sa_family) && + !(ifa->ifa_flags & (IFF_POINTOPOINT | IFF_LOOPBACK))) { + // only IPv6 interfaces that aren't pointtopoint or loopback + struct sockaddr_in6* sin_netmask = (struct sockaddr_in6*)ifa->ifa_netmask; + if (sin_netmask) { + struct sockaddr_in6* sin_addr = (struct sockaddr_in6*)ifa->ifa_addr; + int scope = net::utils::ipv6_scope(sin_addr->sin6_addr.s6_addr); + if (scope == IPV6_SCOPE_GLOBAL) { + struct in6_addr prefix; + memset(&prefix, 0, sizeof(prefix)); + // Get the prefix by combining the address and netmask. + for (size_t i = 0; i < sizeof(prefix); ++i) { + prefix.s6_addr[i] = + sin_addr->sin6_addr.s6_addr[i] & sin_netmask->sin6_addr.s6_addr[i]; + } + + // check if prefix and netmask was already found + auto prefixAndNetmask = std::make_pair(prefix, sin_netmask->sin6_addr); + auto foundPosition = std::find_if( + prefixAndNetmaskStore.begin(), prefixAndNetmaskStore.end(), + [&prefixAndNetmask](prefix_and_netmask current) { + return memcmp(&prefixAndNetmask.first, ¤t.first, sizeof(in6_addr)) == 0 && + memcmp(&prefixAndNetmask.second, ¤t.second, sizeof(in6_addr)) == 0; + }); + if (foundPosition != prefixAndNetmaskStore.end()) { + continue; + } + prefixAndNetmaskStore.push_back(prefixAndNetmask); + } + } + } + } + freeifaddrs(ifap); + } + if (prefixAndNetmaskStore.empty()) { + LOG(("IPv6NetworkId failed")); + return false; + } + + nsNetworkLinkService::HashSortedPrefixesAndNetmasks(prefixAndNetmaskStore, sha1); + + return true; +} + +void nsNetworkLinkService::calculateNetworkIdWithDelay(uint32_t aDelay) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aDelay) { + if (mNetworkIdTimer) { + LOG(("Restart the network id timer.")); + mNetworkIdTimer->Cancel(); + } else { + LOG(("Create the network id timer.")); + mNetworkIdTimer = NS_NewTimer(); + } + mNetworkIdTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT); + return; + } + + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + if (!target) { + return; + } + + MOZ_ALWAYS_SUCCEEDS( + target->Dispatch(NewRunnableMethod("nsNetworkLinkService::calculateNetworkIdInternal", this, + &nsNetworkLinkService::calculateNetworkIdInternal), + NS_DISPATCH_NORMAL)); +} + +NS_IMETHODIMP +nsNetworkLinkService::Notify(nsITimer* aTimer) { + MOZ_ASSERT(aTimer == mNetworkIdTimer); + + mNetworkIdTimer = nullptr; + calculateNetworkIdWithDelay(0); + return NS_OK; +} + +NS_IMETHODIMP +nsNetworkLinkService::GetName(nsACString& aName) { + aName.AssignLiteral("nsNetworkLinkService"); + return NS_OK; +} + +void nsNetworkLinkService::calculateNetworkIdInternal(void) { + MOZ_ASSERT(!NS_IsMainThread(), "Should not be called on the main thread"); + SHA1Sum sha1; + bool idChanged = false; + bool found4 = IPv4NetworkId(&sha1); + bool found6 = IPv6NetworkId(&sha1); + + if (found4 || found6) { + nsAutoCString output; + SeedNetworkId(sha1); + uint8_t digest[SHA1Sum::kHashSize]; + sha1.finish(digest); + nsAutoCString newString(reinterpret_cast<char*>(digest), SHA1Sum::kHashSize); + nsresult rv = Base64Encode(newString, output); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + LOG(("networkid: id %s\n", output.get())); + MutexAutoLock lock(mMutex); + if (mNetworkId != output) { + // new id + if (found4 && !found6) { + Telemetry::Accumulate(Telemetry::NETWORK_ID2, 1); // IPv4 only + } else if (!found4 && found6) { + Telemetry::Accumulate(Telemetry::NETWORK_ID2, 3); // IPv6 only + } else { + Telemetry::Accumulate(Telemetry::NETWORK_ID2, 4); // Both! + } + mNetworkId = output; + idChanged = true; + } else { + // same id + LOG(("Same network id")); + Telemetry::Accumulate(Telemetry::NETWORK_ID2, 2); + } + } else { + // no id + LOG(("No network id")); + MutexAutoLock lock(mMutex); + if (!mNetworkId.IsEmpty()) { + mNetworkId.Truncate(); + idChanged = true; + Telemetry::Accumulate(Telemetry::NETWORK_ID2, 0); + } + } + + // Don't report network change if this is the first time we calculate the id. + static bool initialIDCalculation = true; + if (idChanged && !initialIDCalculation) { + RefPtr<nsNetworkLinkService> self = this; + + NS_DispatchToMainThread( + NS_NewRunnableFunction("nsNetworkLinkService::calculateNetworkIdInternal", + [self]() { self->OnNetworkIdChanged(); })); + } + + initialIDCalculation = false; +} + +NS_IMETHODIMP +nsNetworkLinkService::Observe(nsISupports* subject, const char* topic, const char16_t* data) { + if (!strcmp(topic, "xpcom-shutdown")) { + Shutdown(); + } + + return NS_OK; +} + +/* static */ +void nsNetworkLinkService::NetworkConfigChanged(SCDynamicStoreRef aStoreREf, + CFArrayRef aChangedKeys, void* aInfo) { + LOG(("nsNetworkLinkService::NetworkConfigChanged")); + + bool ipConfigChanged = false; + bool dnsConfigChanged = false; + for (CFIndex i = 0; i < CFArrayGetCount(aChangedKeys); ++i) { + CFStringRef key = static_cast<CFStringRef>(CFArrayGetValueAtIndex(aChangedKeys, i)); + if (CFStringHasSuffix(key, kSCEntNetIPv4) || CFStringHasSuffix(key, kSCEntNetIPv6)) { + ipConfigChanged = true; + } + if (CFStringHasSuffix(key, kSCEntNetDNS)) { + dnsConfigChanged = true; + } + } + + nsNetworkLinkService* service = static_cast<nsNetworkLinkService*>(aInfo); + if (ipConfigChanged) { + service->OnIPConfigChanged(); + } + + if (dnsConfigChanged) { + service->DNSConfigChanged(kDNSSuffixDelayAfterChange); + } +} + +void nsNetworkLinkService::DNSConfigChanged(uint32_t aDelayMs) { + LOG(("nsNetworkLinkService::DNSConfigChanged")); + nsCOMPtr<nsIEventTarget> target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + if (!target) { + return; + } + if (aDelayMs) { + MutexAutoLock lock(mMutex); + nsCOMPtr<nsITimer> timer; + MOZ_ALWAYS_SUCCEEDS(NS_NewTimerWithCallback( + getter_AddRefs(timer), + [self = RefPtr{this}](nsITimer* aTimer) { + self->GetDnsSuffixListInternal(); + + MutexAutoLock lock(self->mMutex); + self->mDNSConfigChangedTimers.RemoveElement(aTimer); + }, + TimeDuration::FromMilliseconds(aDelayMs), nsITimer::TYPE_ONE_SHOT, + "nsNetworkLinkService::GetDnsSuffixListInternal", target)); + mDNSConfigChangedTimers.AppendElement(timer); + } else { + MOZ_ALWAYS_SUCCEEDS(target->Dispatch( + NS_NewRunnableFunction("nsNetworkLinkService::GetDnsSuffixListInternal", + [self = RefPtr{this}]() { self->GetDnsSuffixListInternal(); }))); + } +} + +nsresult nsNetworkLinkService::Init(void) { + nsresult rv; + + nsCOMPtr<nsIObserverService> observerService = + do_GetService("@mozilla.org/observer-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->AddObserver(this, "xpcom-shutdown", false); + NS_ENSURE_SUCCESS(rv, rv); + + if (inet_pton(AF_INET, ROUTE_CHECK_IPV4, &mRouteCheckIPv4) != 1) { + LOG(("Cannot parse address " ROUTE_CHECK_IPV4)); + MOZ_DIAGNOSTIC_ASSERT(false, "Cannot parse address " ROUTE_CHECK_IPV4); + return NS_ERROR_UNEXPECTED; + } + + // If the network reachability API can reach 0.0.0.0 without + // requiring a connection, there is a network interface available. + struct sockaddr_in addr; + bzero(&addr, sizeof(addr)); + addr.sin_len = sizeof(addr); + addr.sin_family = AF_INET; + mReachability = ::SCNetworkReachabilityCreateWithAddress(nullptr, (struct sockaddr*)&addr); + if (!mReachability) { + return NS_ERROR_NOT_AVAILABLE; + } + + SCNetworkReachabilityContext context = {0, this, nullptr, nullptr, nullptr}; + if (!::SCNetworkReachabilitySetCallback(mReachability, ReachabilityChanged, &context)) { + NS_WARNING("SCNetworkReachabilitySetCallback failed."); + ::CFRelease(mReachability); + mReachability = nullptr; + return NS_ERROR_NOT_AVAILABLE; + } + + SCDynamicStoreContext storeContext = {0, this, nullptr, nullptr, nullptr}; + mStoreRef = ::SCDynamicStoreCreate(nullptr, CFSTR("IPAndDNSChangeCallbackSCF"), + NetworkConfigChanged, &storeContext); + + CFStringRef patterns[4] = {nullptr, nullptr, nullptr, nullptr}; + OSStatus err = getErrorCodePtr(mStoreRef); + if (err == noErr) { + // This pattern is "State:/Network/Service/[^/]+/IPv4". + patterns[0] = ::SCDynamicStoreKeyCreateNetworkServiceEntity(nullptr, kSCDynamicStoreDomainState, + kSCCompAnyRegex, kSCEntNetIPv4); + // This pattern is "State:/Network/Service/[^/]+/IPv6". + patterns[1] = ::SCDynamicStoreKeyCreateNetworkServiceEntity(nullptr, kSCDynamicStoreDomainState, + kSCCompAnyRegex, kSCEntNetIPv6); + // This pattern is "State:/Network/Service/[^/]+/DNS". + patterns[2] = ::SCDynamicStoreKeyCreateNetworkServiceEntity(nullptr, kSCDynamicStoreDomainState, + kSCCompAnyRegex, kSCEntNetDNS); + // This pattern is "Setup:/Network/Service/[^/]+/DNS". + patterns[3] = ::SCDynamicStoreKeyCreateNetworkServiceEntity(nullptr, kSCDynamicStoreDomainSetup, + kSCCompAnyRegex, kSCEntNetDNS); + if (!patterns[0] || !patterns[1] || !patterns[2] || !patterns[3]) { + err = -1; + } + } + + CFArrayRef patternList = nullptr; + // Create a pattern list containing just one pattern, + // then tell SCF that we want to watch changes in keys + // that match that pattern list, then create our run loop + // source. + if (err == noErr) { + patternList = ::CFArrayCreate(nullptr, (const void**)patterns, 4, &kCFTypeArrayCallBacks); + if (!patternList) { + err = -1; + } + } + if (err == noErr) { + err = getErrorCodeBool(::SCDynamicStoreSetNotificationKeys(mStoreRef, nullptr, patternList)); + } + + if (err == noErr) { + mRunLoopSource = ::SCDynamicStoreCreateRunLoopSource(nullptr, mStoreRef, 0); + err = getErrorCodePtr(mRunLoopSource); + } + + CFReleaseSafe(patterns[0]); + CFReleaseSafe(patterns[1]); + CFReleaseSafe(patterns[2]); + CFReleaseSafe(patterns[3]); + CFReleaseSafe(patternList); + + if (err != noErr) { + CFReleaseSafe(mStoreRef); + return NS_ERROR_NOT_AVAILABLE; + } + + // Get the current run loop. This service is initialized at startup, + // so we shouldn't run in to any problems with modal dialog run loops. + mCFRunLoop = [[NSRunLoop currentRunLoop] getCFRunLoop]; + if (!mCFRunLoop) { + NS_WARNING("Could not get current run loop."); + ::CFRelease(mReachability); + mReachability = nullptr; + return NS_ERROR_NOT_AVAILABLE; + } + ::CFRetain(mCFRunLoop); + + ::CFRunLoopAddSource(mCFRunLoop, mRunLoopSource, kCFRunLoopDefaultMode); + + if (!::SCNetworkReachabilityScheduleWithRunLoop(mReachability, mCFRunLoop, + kCFRunLoopDefaultMode)) { + NS_WARNING("SCNetworkReachabilityScheduleWIthRunLoop failed."); + ::CFRelease(mReachability); + mReachability = nullptr; + ::CFRelease(mCFRunLoop); + mCFRunLoop = nullptr; + return NS_ERROR_NOT_AVAILABLE; + } + UpdateReachability(); + + calculateNetworkIdWithDelay(0); + + DNSConfigChanged(0); + + return NS_OK; +} + +nsresult nsNetworkLinkService::Shutdown() { + if (!::SCNetworkReachabilityUnscheduleFromRunLoop(mReachability, mCFRunLoop, + kCFRunLoopDefaultMode)) { + NS_WARNING("SCNetworkReachabilityUnscheduleFromRunLoop failed."); + } + + CFRunLoopRemoveSource(mCFRunLoop, mRunLoopSource, kCFRunLoopDefaultMode); + + ::CFRelease(mReachability); + mReachability = nullptr; + + ::CFRelease(mCFRunLoop); + mCFRunLoop = nullptr; + + ::CFRelease(mStoreRef); + mStoreRef = nullptr; + + ::CFRelease(mRunLoopSource); + mRunLoopSource = nullptr; + + if (mNetworkIdTimer) { + mNetworkIdTimer->Cancel(); + mNetworkIdTimer = nullptr; + } + + nsTArray<nsCOMPtr<nsITimer>> dnsConfigChangedTimers; + { + MutexAutoLock lock(mMutex); + dnsConfigChangedTimers = std::move(mDNSConfigChangedTimers); + mDNSConfigChangedTimers.Clear(); + } + for (const auto& timer : dnsConfigChangedTimers) { + timer->Cancel(); + } + + return NS_OK; +} + +void nsNetworkLinkService::UpdateReachability() { + if (!mReachability) { + return; + } + + SCNetworkConnectionFlags flags; + if (!::SCNetworkReachabilityGetFlags(mReachability, &flags)) { + mStatusKnown = false; + return; + } + + bool reachable = (flags & kSCNetworkFlagsReachable) != 0; + bool needsConnection = (flags & kSCNetworkFlagsConnectionRequired) != 0; + + mLinkUp = (reachable && !needsConnection); + mStatusKnown = true; +} + +void nsNetworkLinkService::OnIPConfigChanged() { + MOZ_ASSERT(NS_IsMainThread()); + + calculateNetworkIdWithDelay(kNetworkIdDelayAfterChange); + if (!StaticPrefs::network_notify_changed()) { + return; + } + + NotifyObservers(NS_NETWORK_LINK_TOPIC, NS_NETWORK_LINK_DATA_CHANGED); +} + +void nsNetworkLinkService::OnNetworkIdChanged() { + MOZ_ASSERT(NS_IsMainThread()); + + NotifyObservers(NS_NETWORK_ID_CHANGED_TOPIC, nullptr); +} + +void nsNetworkLinkService::OnReachabilityChanged() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mStatusKnown) { + NotifyObservers(NS_NETWORK_LINK_TOPIC, NS_NETWORK_LINK_DATA_UNKNOWN); + return; + } + + NotifyObservers(NS_NETWORK_LINK_TOPIC, + mLinkUp ? NS_NETWORK_LINK_DATA_UP : NS_NETWORK_LINK_DATA_DOWN); +} + +void nsNetworkLinkService::NotifyObservers(const char* aTopic, const char* aData) { + MOZ_ASSERT(NS_IsMainThread()); + + LOG(("nsNetworkLinkService::NotifyObservers: topic:%s data:%s\n", aTopic, aData ? aData : "")); + + nsCOMPtr<nsIObserverService> observerService = mozilla::services::GetObserverService(); + + if (observerService) { + observerService->NotifyObservers(static_cast<nsINetworkLinkService*>(this), aTopic, + aData ? NS_ConvertASCIItoUTF16(aData).get() : nullptr); + } +} + +/* static */ +void nsNetworkLinkService::ReachabilityChanged(SCNetworkReachabilityRef target, + SCNetworkConnectionFlags flags, void* info) { + LOG(("nsNetworkLinkService::ReachabilityChanged")); + nsNetworkLinkService* service = static_cast<nsNetworkLinkService*>(info); + + service->UpdateReachability(); + service->OnReachabilityChanged(); + service->calculateNetworkIdWithDelay(kNetworkIdDelayAfterChange); + // If a new interface is up or the order of interfaces is changed, we should + // update the DNS suffix list. + service->DNSConfigChanged(0); +} |