netwerk/system/mac/nsNetworkLinkService.mm
author Mozilla Releng Treescript <release+treescript@mozilla.org>
Fri, 20 May 2022 19:04:40 +0000
changeset 618368 3b0030f76f1ee69138d29f114d5221f21c55e7e5
parent 611721 ba728a4ee829948f6a564630ed768e53656a14f9
permissions -rw-r--r--
no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD de -> 1d458b9f128b124a25679e755f61139726a77caf dsb -> d3afb988e0d6b558a8a590658da39eb15b2f0b18 et -> 77d8cd6a89d77a9356bc597e476cd2cc764cd607 hsb -> a67aba9c3b33f8bc097d742bd70ea98ca80a1335 hu -> ccf9d887f4eb7e4bac53afde57598686a1e255b9 hye -> ed3709ec02d90cca3ebf3237ea6d6411353e2559 it -> d544065a45d9b684beb7f60bbeaf30fd1a696e69 ja -> 17e2f87f08c519add504e8f4265e0f7b6524a34a ja-JP-mac -> 0e30fddf50953f46b80dbc99825270568b1a0d65 oc -> e114f06272b1c9377d19ca013aa6a51509cb719a pt-PT -> f7bf8255666f78337b22b3206516736daf222d40 zh-CN -> ff1ee767ed480c30d6aaefa00c65c8f62477cc18

/* -*- 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 "../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;
    }
    needed += needed / 8;

    auto tmp = MakeUnique<char[]>(needed);
    memcpy(&tmp[0], &buf[0], needed);
    buf = std::move(tmp);
  }
  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, &current.first, sizeof(in6_addr)) == 0 &&
                         memcmp(&prefixAndNetmask.second, &current.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) {
    // This 'addition' could potentially be a fixed number from the
    // profile or something.
    nsAutoCString addition("local-rubbish");
    nsAutoCString output;
    sha1.update(addition.get(), addition.Length());
    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);
}