diff options
Diffstat (limited to '')
-rw-r--r-- | netwerk/system/netlink/NetlinkService.cpp | 1903 |
1 files changed, 1903 insertions, 0 deletions
diff --git a/netwerk/system/netlink/NetlinkService.cpp b/netwerk/system/netlink/NetlinkService.cpp new file mode 100644 index 0000000000..9e882e5ab6 --- /dev/null +++ b/netwerk/system/netlink/NetlinkService.cpp @@ -0,0 +1,1903 @@ +/* -*- 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/. */ + +#include <arpa/inet.h> +#include <netinet/ether.h> +#include <net/if.h> +#include <poll.h> +#include <unistd.h> +#include <linux/rtnetlink.h> + +#include "nsThreadUtils.h" +#include "NetlinkService.h" +#include "nsIThread.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "mozilla/Logging.h" +#include "../../base/IPv6Utils.h" +#include "../LinkServiceCommon.h" +#include "../NetworkLinkServiceDefines.h" + +#include "mozilla/Base64.h" +#include "mozilla/FunctionTypeTraits.h" +#include "mozilla/ProfilerThreadSleep.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" + +#if defined(HAVE_RES_NINIT) +# include <netinet/in.h> +# include <resolv.h> +#endif + +namespace mozilla::net { + +template <typename F> +static auto eintr_retry(F&& func) -> + typename FunctionTypeTraits<decltype(func)>::ReturnType { + typename FunctionTypeTraits<decltype(func)>::ReturnType _rc; + do { + _rc = func(); + } while (_rc == -1 && errno == EINTR); + return _rc; +} + +#define EINTR_RETRY(expr) eintr_retry([&]() { return expr; }) + +// period during which to absorb subsequent network change events, in +// milliseconds +static const unsigned int kNetworkChangeCoalescingPeriod = 1000; + +static LazyLogModule gNlSvcLog("NetlinkService"); +#define LOG(args) MOZ_LOG(gNlSvcLog, mozilla::LogLevel::Debug, args) + +#undef LOG_ENABLED +#define LOG_ENABLED() MOZ_LOG_TEST(gNlSvcLog, mozilla::LogLevel::Debug) + +using in_common_addr = union { + struct in_addr addr4; + struct in6_addr addr6; +}; + +static void GetAddrStr(const in_common_addr* aAddr, uint8_t aFamily, + nsACString& _retval) { + char addr[INET6_ADDRSTRLEN]; + addr[0] = 0; + + if (aFamily == AF_INET) { + inet_ntop(AF_INET, &(aAddr->addr4), addr, INET_ADDRSTRLEN); + } else { + inet_ntop(AF_INET6, &(aAddr->addr6), addr, INET6_ADDRSTRLEN); + } + _retval.Assign(addr); +} + +class NetlinkAddress { + public: + NetlinkAddress() = default; + + uint8_t Family() const { return mIfam.ifa_family; } + uint32_t GetIndex() const { return mIfam.ifa_index; } + uint8_t GetPrefixLen() const { return mIfam.ifa_prefixlen; } + bool ScopeIsUniverse() const { return mIfam.ifa_scope == RT_SCOPE_UNIVERSE; } + const in_common_addr* GetAddrPtr() const { return &mAddr; } + + bool MsgEquals(const NetlinkAddress& aOther) const { + return !memcmp(&mIfam, &(aOther.mIfam), sizeof(mIfam)); + } + + bool Equals(const NetlinkAddress& aOther) const { + if (mIfam.ifa_family != aOther.mIfam.ifa_family) { + return false; + } + if (mIfam.ifa_index != aOther.mIfam.ifa_index) { + // addresses are different when they are on a different interface + return false; + } + if (mIfam.ifa_prefixlen != aOther.mIfam.ifa_prefixlen) { + // It's possible to have two equal addresses with a different netmask on + // the same interface, so we need to check prefixlen too. + return false; + } + size_t addrSize = (mIfam.ifa_family == AF_INET) ? sizeof(mAddr.addr4) + : sizeof(mAddr.addr6); + return memcmp(&mAddr, aOther.GetAddrPtr(), addrSize) == 0; + } + + bool ContainsAddr(const in_common_addr* aAddr) { + int32_t addrSize = (mIfam.ifa_family == AF_INET) + ? (int32_t)sizeof(mAddr.addr4) + : (int32_t)sizeof(mAddr.addr6); + uint8_t maskit[] = {0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe}; + int32_t bits = mIfam.ifa_prefixlen; + if (bits > addrSize * 8) { + MOZ_ASSERT(false, "Unexpected prefix length!"); + LOG(("Unexpected prefix length %d, maximum for this family is %d", bits, + addrSize * 8)); + return false; + } + for (int32_t i = 0; i < addrSize; i++) { + uint8_t mask = (bits >= 8) ? 0xff : maskit[bits]; + if ((((unsigned char*)aAddr)[i] & mask) != + (((unsigned char*)(&mAddr))[i] & mask)) { + return false; + } + bits -= 8; + if (bits <= 0) { + return true; + } + } + return true; + } + + bool Init(struct nlmsghdr* aNlh) { + struct ifaddrmsg* ifam; + struct rtattr* attr; + int len; + + ifam = (ifaddrmsg*)NLMSG_DATA(aNlh); + len = IFA_PAYLOAD(aNlh); + + if (ifam->ifa_family != AF_INET && ifam->ifa_family != AF_INET6) { + return false; + } + + bool hasAddr = false; + for (attr = IFA_RTA(ifam); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) { + if (attr->rta_type == IFA_ADDRESS || attr->rta_type == IFA_LOCAL) { + memcpy(&mAddr, RTA_DATA(attr), + ifam->ifa_family == AF_INET ? sizeof(mAddr.addr4) + : sizeof(mAddr.addr6)); + hasAddr = true; + if (attr->rta_type == IFA_LOCAL) { + // local address is preferred, so don't continue parsing other + // attributes + break; + } + } + } + + if (!hasAddr) { + return false; + } + + memcpy(&mIfam, (ifaddrmsg*)NLMSG_DATA(aNlh), sizeof(mIfam)); + return true; + } + + private: + in_common_addr mAddr; + struct ifaddrmsg mIfam; +}; + +class NetlinkNeighbor { + public: + NetlinkNeighbor() = default; + + uint8_t Family() const { return mNeigh.ndm_family; } + uint32_t GetIndex() const { return mNeigh.ndm_ifindex; } + const in_common_addr* GetAddrPtr() const { return &mAddr; } + const uint8_t* GetMACPtr() const { return mMAC; } + bool HasMAC() const { return mHasMAC; }; + + void GetAsString(nsACString& _retval) const { + nsAutoCString addrStr; + _retval.Assign("addr="); + GetAddrStr(&mAddr, mNeigh.ndm_family, addrStr); + _retval.Append(addrStr); + if (mNeigh.ndm_family == AF_INET) { + _retval.Append(" family=AF_INET if="); + } else { + _retval.Append(" family=AF_INET6 if="); + } + _retval.AppendInt(mNeigh.ndm_ifindex); + if (mHasMAC) { + _retval.Append(" mac="); + _retval.Append(nsPrintfCString("%02x:%02x:%02x:%02x:%02x:%02x", mMAC[0], + mMAC[1], mMAC[2], mMAC[3], mMAC[4], + mMAC[5])); + } + } + + bool Init(struct nlmsghdr* aNlh) { + struct ndmsg* neigh; + struct rtattr* attr; + int len; + + neigh = (ndmsg*)NLMSG_DATA(aNlh); + len = aNlh->nlmsg_len - NLMSG_LENGTH(sizeof(*neigh)); + + if (neigh->ndm_family != AF_INET && neigh->ndm_family != AF_INET6) { + return false; + } + + bool hasDST = false; + for (attr = RTM_RTA(neigh); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) { + if (attr->rta_type == NDA_LLADDR) { + memcpy(mMAC, RTA_DATA(attr), ETH_ALEN); + mHasMAC = true; + } + + if (attr->rta_type == NDA_DST) { + memcpy(&mAddr, RTA_DATA(attr), + neigh->ndm_family == AF_INET ? sizeof(mAddr.addr4) + : sizeof(mAddr.addr6)); + hasDST = true; + } + } + + if (!hasDST) { + return false; + } + + memcpy(&mNeigh, (ndmsg*)NLMSG_DATA(aNlh), sizeof(mNeigh)); + return true; + } + + private: + bool mHasMAC{false}; + uint8_t mMAC[ETH_ALEN]{}; + in_common_addr mAddr{}; + struct ndmsg mNeigh {}; +}; + +class NetlinkLink { + public: + NetlinkLink() = default; + + bool IsUp() const { + return (mIface.ifi_flags & IFF_RUNNING) && + !(mIface.ifi_flags & IFF_LOOPBACK); + } + + void GetName(nsACString& _retval) const { _retval = mName; } + bool IsTypeEther() const { return mIface.ifi_type == ARPHRD_ETHER; } + uint32_t GetIndex() const { return mIface.ifi_index; } + uint32_t GetFlags() const { return mIface.ifi_flags; } + uint16_t GetType() const { return mIface.ifi_type; } + + bool Init(struct nlmsghdr* aNlh) { + struct ifinfomsg* iface; + struct rtattr* attr; + int len; + + iface = (ifinfomsg*)NLMSG_DATA(aNlh); + len = aNlh->nlmsg_len - NLMSG_LENGTH(sizeof(*iface)); + + bool hasName = false; + for (attr = IFLA_RTA(iface); RTA_OK(attr, len); + attr = RTA_NEXT(attr, len)) { + if (attr->rta_type == IFLA_IFNAME) { + mName.Assign((char*)RTA_DATA(attr)); + hasName = true; + break; + } + } + + if (!hasName) { + return false; + } + + memcpy(&mIface, (ifinfomsg*)NLMSG_DATA(aNlh), sizeof(mIface)); + return true; + } + + private: + nsCString mName; + struct ifinfomsg mIface {}; +}; + +class NetlinkRoute { + public: + NetlinkRoute() + : mHasGWAddr(false), + mHasPrefSrcAddr(false), + mHasDstAddr(false), + mHasOif(false), + mHasPrio(false) {} + + bool IsUnicast() const { return mRtm.rtm_type == RTN_UNICAST; } + bool ScopeIsUniverse() const { return mRtm.rtm_scope == RT_SCOPE_UNIVERSE; } + bool IsDefault() const { return mRtm.rtm_dst_len == 0; } + bool HasOif() const { return mHasOif; } + uint8_t Oif() const { return mOif; } + uint8_t Family() const { return mRtm.rtm_family; } + bool HasPrefSrcAddr() const { return mHasPrefSrcAddr; } + const in_common_addr* GetGWAddrPtr() const { + return mHasGWAddr ? &mGWAddr : nullptr; + } + const in_common_addr* GetPrefSrcAddrPtr() const { + return mHasPrefSrcAddr ? &mPrefSrcAddr : nullptr; + } + + bool Equals(const NetlinkRoute& aOther) const { + size_t addrSize = (mRtm.rtm_family == AF_INET) ? sizeof(mDstAddr.addr4) + : sizeof(mDstAddr.addr6); + if (memcmp(&mRtm, &(aOther.mRtm), sizeof(mRtm)) != 0) { + return false; + } + if (mHasOif != aOther.mHasOif || mOif != aOther.mOif) { + return false; + } + if (mHasPrio != aOther.mHasPrio || mPrio != aOther.mPrio) { + return false; + } + if ((mHasGWAddr != aOther.mHasGWAddr) || + (mHasGWAddr && memcmp(&mGWAddr, &(aOther.mGWAddr), addrSize) != 0)) { + return false; + } + if ((mHasDstAddr != aOther.mHasDstAddr) || + (mHasDstAddr && memcmp(&mDstAddr, &(aOther.mDstAddr), addrSize) != 0)) { + return false; + } + if ((mHasPrefSrcAddr != aOther.mHasPrefSrcAddr) || + (mHasPrefSrcAddr && + memcmp(&mPrefSrcAddr, &(aOther.mPrefSrcAddr), addrSize) != 0)) { + return false; + } + return true; + } + + bool GatewayEquals(const NetlinkNeighbor& aNeigh) const { + if (!mHasGWAddr) { + return false; + } + if (aNeigh.Family() != mRtm.rtm_family) { + return false; + } + size_t addrSize = (mRtm.rtm_family == AF_INET) ? sizeof(mGWAddr.addr4) + : sizeof(mGWAddr.addr6); + return memcmp(&mGWAddr, aNeigh.GetAddrPtr(), addrSize) == 0; + } + + bool GatewayEquals(const NetlinkRoute* aRoute) const { + if (!mHasGWAddr || !aRoute->mHasGWAddr) { + return false; + } + if (mRtm.rtm_family != aRoute->mRtm.rtm_family) { + return false; + } + size_t addrSize = (mRtm.rtm_family == AF_INET) ? sizeof(mGWAddr.addr4) + : sizeof(mGWAddr.addr6); + return memcmp(&mGWAddr, &(aRoute->mGWAddr), addrSize) == 0; + } + + bool PrefSrcAddrEquals(const NetlinkAddress& aAddress) const { + if (!mHasPrefSrcAddr) { + return false; + } + if (mRtm.rtm_family != aAddress.Family()) { + return false; + } + size_t addrSize = (mRtm.rtm_family == AF_INET) ? sizeof(mPrefSrcAddr.addr4) + : sizeof(mPrefSrcAddr.addr6); + return memcmp(&mPrefSrcAddr, aAddress.GetAddrPtr(), addrSize) == 0; + } + + void GetAsString(nsACString& _retval) const { + nsAutoCString addrStr; + _retval.Assign("table="); + _retval.AppendInt(mRtm.rtm_table); + _retval.Append(" type="); + _retval.AppendInt(mRtm.rtm_type); + _retval.Append(" scope="); + _retval.AppendInt(mRtm.rtm_scope); + if (mRtm.rtm_family == AF_INET) { + _retval.Append(" family=AF_INET dst="); + addrStr.Assign("0.0.0.0/"); + } else { + _retval.Append(" family=AF_INET6 dst="); + addrStr.Assign("::/"); + } + if (mHasDstAddr) { + GetAddrStr(&mDstAddr, mRtm.rtm_family, addrStr); + addrStr.Append("/"); + } + _retval.Append(addrStr); + _retval.AppendInt(mRtm.rtm_dst_len); + if (mHasPrefSrcAddr) { + _retval.Append(" src="); + GetAddrStr(&mPrefSrcAddr, mRtm.rtm_family, addrStr); + _retval.Append(addrStr); + } + if (mHasGWAddr) { + _retval.Append(" via="); + GetAddrStr(&mGWAddr, mRtm.rtm_family, addrStr); + _retval.Append(addrStr); + } + if (mHasOif) { + _retval.Append(" oif="); + _retval.AppendInt(mOif); + } + if (mHasPrio) { + _retval.Append(" prio="); + _retval.AppendInt(mPrio); + } + } + + bool Init(struct nlmsghdr* aNlh) { + struct rtmsg* rtm; + struct rtattr* attr; + int len; + + rtm = (rtmsg*)NLMSG_DATA(aNlh); + len = RTM_PAYLOAD(aNlh); + + if (rtm->rtm_family != AF_INET && rtm->rtm_family != AF_INET6) { + return false; + } + + for (attr = RTM_RTA(rtm); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) { + if (attr->rta_type == RTA_DST) { + memcpy(&mDstAddr, RTA_DATA(attr), + (rtm->rtm_family == AF_INET) ? sizeof(mDstAddr.addr4) + : sizeof(mDstAddr.addr6)); + mHasDstAddr = true; + } else if (attr->rta_type == RTA_GATEWAY) { + memcpy(&mGWAddr, RTA_DATA(attr), + (rtm->rtm_family == AF_INET) ? sizeof(mGWAddr.addr4) + : sizeof(mGWAddr.addr6)); + mHasGWAddr = true; + } else if (attr->rta_type == RTA_PREFSRC) { + memcpy(&mPrefSrcAddr, RTA_DATA(attr), + (rtm->rtm_family == AF_INET) ? sizeof(mPrefSrcAddr.addr4) + : sizeof(mPrefSrcAddr.addr6)); + mHasPrefSrcAddr = true; + } else if (attr->rta_type == RTA_OIF) { + mOif = *(uint32_t*)RTA_DATA(attr); + mHasOif = true; + } else if (attr->rta_type == RTA_PRIORITY) { + mPrio = *(uint32_t*)RTA_DATA(attr); + mHasPrio = true; + } + } + + memcpy(&mRtm, (rtmsg*)NLMSG_DATA(aNlh), sizeof(mRtm)); + return true; + } + + private: + bool mHasGWAddr : 1; + bool mHasPrefSrcAddr : 1; + bool mHasDstAddr : 1; + bool mHasOif : 1; + bool mHasPrio : 1; + + in_common_addr mGWAddr{}; + in_common_addr mDstAddr{}; + in_common_addr mPrefSrcAddr{}; + + uint32_t mOif{}; + uint32_t mPrio{}; + struct rtmsg mRtm {}; +}; + +class NetlinkMsg { + public: + static uint8_t const kGenMsg = 1; + static uint8_t const kRtMsg = 2; + + NetlinkMsg() = default; + virtual ~NetlinkMsg() = default; + + virtual bool Send(int aFD) = 0; + virtual bool IsPending() { return mIsPending; } + virtual uint32_t SeqId() = 0; + virtual uint8_t Family() = 0; + virtual uint8_t MsgType() = 0; + + protected: + bool SendRequest(int aFD, void* aRequest, uint32_t aRequestLength) { + MOZ_ASSERT(!mIsPending, "Request has been already sent!"); + + struct sockaddr_nl kernel {}; + memset(&kernel, 0, sizeof(kernel)); + kernel.nl_family = AF_NETLINK; + kernel.nl_groups = 0; + + struct iovec io {}; + memset(&io, 0, sizeof(io)); + io.iov_base = aRequest; + io.iov_len = aRequestLength; + + struct msghdr rtnl_msg {}; + memset(&rtnl_msg, 0, sizeof(rtnl_msg)); + rtnl_msg.msg_iov = &io; + rtnl_msg.msg_iovlen = 1; + rtnl_msg.msg_name = &kernel; + rtnl_msg.msg_namelen = sizeof(kernel); + + ssize_t rc = EINTR_RETRY(sendmsg(aFD, (struct msghdr*)&rtnl_msg, 0)); + if (rc > 0 && (uint32_t)rc == aRequestLength) { + mIsPending = true; + } + + return mIsPending; + } + + bool mIsPending{false}; +}; + +class NetlinkGenMsg : public NetlinkMsg { + public: + NetlinkGenMsg(uint16_t aMsgType, uint8_t aFamily, uint32_t aSeqId) { + memset(&mReq, 0, sizeof(mReq)); + + mReq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg)); + mReq.hdr.nlmsg_type = aMsgType; + mReq.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + mReq.hdr.nlmsg_seq = aSeqId; + mReq.hdr.nlmsg_pid = 0; + + mReq.gen.rtgen_family = aFamily; + } + + virtual bool Send(int aFD) { + return SendRequest(aFD, &mReq, mReq.hdr.nlmsg_len); + } + + virtual uint32_t SeqId() { return mReq.hdr.nlmsg_seq; } + virtual uint8_t Family() { return mReq.gen.rtgen_family; } + virtual uint8_t MsgType() { return kGenMsg; } + + private: + struct { + struct nlmsghdr hdr; + struct rtgenmsg gen; + } mReq{}; +}; + +class NetlinkRtMsg : public NetlinkMsg { + public: + NetlinkRtMsg(uint8_t aFamily, void* aAddress, uint32_t aSeqId) { + MOZ_ASSERT(aFamily == AF_INET || aFamily == AF_INET6); + + memset(&mReq, 0, sizeof(mReq)); + + mReq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + mReq.hdr.nlmsg_type = RTM_GETROUTE; + mReq.hdr.nlmsg_flags = NLM_F_REQUEST; + mReq.hdr.nlmsg_seq = aSeqId; + mReq.hdr.nlmsg_pid = 0; + + mReq.rtm.rtm_family = aFamily; + mReq.rtm.rtm_flags = 0; + mReq.rtm.rtm_dst_len = aFamily == AF_INET ? 32 : 128; + + struct rtattr* rta; + rta = (struct rtattr*)(((char*)&mReq) + NLMSG_ALIGN(mReq.hdr.nlmsg_len)); + rta->rta_type = RTA_DST; + size_t addrSize = + aFamily == AF_INET ? sizeof(struct in_addr) : sizeof(struct in6_addr); + rta->rta_len = RTA_LENGTH(addrSize); + memcpy(RTA_DATA(rta), aAddress, addrSize); + mReq.hdr.nlmsg_len = NLMSG_ALIGN(mReq.hdr.nlmsg_len) + RTA_LENGTH(addrSize); + } + + virtual bool Send(int aFD) { + return SendRequest(aFD, &mReq, mReq.hdr.nlmsg_len); + } + + virtual uint32_t SeqId() { return mReq.hdr.nlmsg_seq; } + virtual uint8_t Family() { return mReq.rtm.rtm_family; } + virtual uint8_t MsgType() { return kRtMsg; } + + private: + struct { + struct nlmsghdr hdr; + struct rtmsg rtm; + unsigned char data[1024]; + } mReq{}; +}; + +NetlinkService::LinkInfo::LinkInfo(UniquePtr<NetlinkLink>&& aLink) + : mLink(std::move(aLink)), mIsUp(false) {} + +NetlinkService::LinkInfo::~LinkInfo() = default; + +bool NetlinkService::LinkInfo::UpdateStatus() { + LOG(("NetlinkService::LinkInfo::UpdateStatus")); + + bool oldIsUp = mIsUp; + mIsUp = false; + + if (!mLink->IsUp()) { + // The link is not up or is a loopback + LOG(("The link is down or is a loopback")); + } else { + // Link is up when there is non-local address associated with it. + for (uint32_t i = 0; i < mAddresses.Length(); ++i) { + if (LOG_ENABLED()) { + nsAutoCString dbgStr; + GetAddrStr(mAddresses[i]->GetAddrPtr(), mAddresses[i]->Family(), + dbgStr); + LOG(("checking address %s", dbgStr.get())); + } + if (mAddresses[i]->ScopeIsUniverse()) { + mIsUp = true; + LOG(("global address found")); + break; + } + } + } + + return mIsUp == oldIsUp; +} + +NS_IMPL_ISUPPORTS(NetlinkService, nsIRunnable) + +NetlinkService::NetlinkService() : mPid(getpid()) {} + +NetlinkService::~NetlinkService() { + MOZ_ASSERT(!mThread, "NetlinkService thread shutdown failed"); + + if (mShutdownPipe[0] != -1) { + EINTR_RETRY(close(mShutdownPipe[0])); + } + if (mShutdownPipe[1] != -1) { + EINTR_RETRY(close(mShutdownPipe[1])); + } +} + +void NetlinkService::OnNetlinkMessage(int aNetlinkSocket) { + // The buffer size 4096 is a common page size, which is a recommended limit + // for netlink messages. + char buffer[4096]; + + struct sockaddr_nl kernel {}; + memset(&kernel, 0, sizeof(kernel)); + kernel.nl_family = AF_NETLINK; + kernel.nl_groups = 0; + + struct iovec io {}; + memset(&io, 0, sizeof(io)); + io.iov_base = buffer; + io.iov_len = sizeof(buffer); + + struct msghdr rtnl_reply {}; + memset(&rtnl_reply, 0, sizeof(rtnl_reply)); + rtnl_reply.msg_iov = &io; + rtnl_reply.msg_iovlen = 1; + rtnl_reply.msg_name = &kernel; + rtnl_reply.msg_namelen = sizeof(kernel); + + ssize_t rc = EINTR_RETRY(recvmsg(aNetlinkSocket, &rtnl_reply, MSG_DONTWAIT)); + if (rc < 0) { + return; + } + size_t netlink_bytes = rc; + + struct nlmsghdr* nlh = reinterpret_cast<struct nlmsghdr*>(buffer); + + for (; NLMSG_OK(nlh, netlink_bytes); nlh = NLMSG_NEXT(nlh, netlink_bytes)) { + // If PID in the message is our PID, then it's a response to our request. + // Otherwise it's a multicast message. + bool isResponse = (pid_t)nlh->nlmsg_pid == mPid; + if (isResponse) { + if (!mOutgoingMessages.Length() || !mOutgoingMessages[0]->IsPending()) { + // There is no enqueued message pending? + LOG(( + "Ignoring message seq_id %u, because there is no associated message" + " pending", + nlh->nlmsg_seq)); + continue; + } + + if (mOutgoingMessages[0]->SeqId() != nlh->nlmsg_seq) { + LOG(("Received unexpected seq_id [received=%u, expected=%u]", + nlh->nlmsg_seq, mOutgoingMessages[0]->SeqId())); + RemovePendingMsg(); + continue; + } + } + + switch (nlh->nlmsg_type) { + case NLMSG_DONE: /* Message signalling end of dump for responses to + request containing NLM_F_DUMP flag */ + LOG(("received NLMSG_DONE")); + if (isResponse) { + RemovePendingMsg(); + } + break; + case NLMSG_ERROR: + LOG(("received NLMSG_ERROR")); + if (isResponse) { + if (mOutgoingMessages[0]->MsgType() == NetlinkMsg::kRtMsg) { + OnRouteCheckResult(nullptr); + } + RemovePendingMsg(); + } + break; + case RTM_NEWLINK: + case RTM_DELLINK: + MOZ_ASSERT(!isResponse || + (nlh->nlmsg_flags & NLM_F_MULTI) == NLM_F_MULTI); + OnLinkMessage(nlh); + break; + case RTM_NEWADDR: + case RTM_DELADDR: + MOZ_ASSERT(!isResponse || + (nlh->nlmsg_flags & NLM_F_MULTI) == NLM_F_MULTI); + OnAddrMessage(nlh); + break; + case RTM_NEWROUTE: + case RTM_DELROUTE: + if (isResponse && ((nlh->nlmsg_flags & NLM_F_MULTI) != NLM_F_MULTI)) { + // If it's not multipart message, then it must be response to a route + // check. + MOZ_ASSERT(mOutgoingMessages[0]->MsgType() == NetlinkMsg::kRtMsg); + OnRouteCheckResult(nlh); + RemovePendingMsg(); + } else { + OnRouteMessage(nlh); + } + break; + case RTM_NEWNEIGH: + case RTM_DELNEIGH: + MOZ_ASSERT(!isResponse || + (nlh->nlmsg_flags & NLM_F_MULTI) == NLM_F_MULTI); + OnNeighborMessage(nlh); + break; + default: + break; + } + } +} + +void NetlinkService::OnLinkMessage(struct nlmsghdr* aNlh) { + LOG(("NetlinkService::OnLinkMessage [type=%s]", + aNlh->nlmsg_type == RTM_NEWLINK ? "new" : "del")); + + UniquePtr<NetlinkLink> link(new NetlinkLink()); + if (!link->Init(aNlh)) { + return; + } + + const uint32_t linkIndex = link->GetIndex(); + mLinks.WithEntryHandle(linkIndex, [&](auto&& entry) { + nsAutoCString linkName; + link->GetName(linkName); + + if (aNlh->nlmsg_type == RTM_NEWLINK) { + if (!entry) { + LOG(("Creating new link [index=%u, name=%s, flags=%u, type=%u]", + linkIndex, linkName.get(), link->GetFlags(), link->GetType())); + entry.Insert(MakeUnique<LinkInfo>(std::move(link))); + } else { + LOG(("Updating link [index=%u, name=%s, flags=%u, type=%u]", linkIndex, + linkName.get(), link->GetFlags(), link->GetType())); + + auto* linkInfo = entry->get(); + + // Check whether administrative state has changed. + if (linkInfo->mLink->GetFlags() & IFF_UP && + !(link->GetFlags() & IFF_UP)) { + LOG((" link went down")); + // If the link went down, remove all routes and neighbors, but keep + // addresses. + linkInfo->mDefaultRoutes.Clear(); + linkInfo->mNeighbors.Clear(); + } + + linkInfo->mLink = std::move(link); + linkInfo->UpdateStatus(); + } + } else { + if (!entry) { + // This can happen during startup + LOG(("Link info doesn't exist [index=%u, name=%s]", linkIndex, + linkName.get())); + } else { + LOG(("Removing link [index=%u, name=%s]", linkIndex, linkName.get())); + entry.Remove(); + } + } + }); +} + +void NetlinkService::OnAddrMessage(struct nlmsghdr* aNlh) { + LOG(("NetlinkService::OnAddrMessage [type=%s]", + aNlh->nlmsg_type == RTM_NEWADDR ? "new" : "del")); + + UniquePtr<NetlinkAddress> address(new NetlinkAddress()); + if (!address->Init(aNlh)) { + return; + } + + uint32_t ifIdx = address->GetIndex(); + + nsAutoCString addrStr; + GetAddrStr(address->GetAddrPtr(), address->Family(), addrStr); + + LinkInfo* linkInfo = nullptr; + mLinks.Get(ifIdx, &linkInfo); + if (!linkInfo) { + // This can happen during startup + LOG(("Cannot find link info [ifIdx=%u, addr=%s/%u", ifIdx, addrStr.get(), + address->GetPrefixLen())); + return; + } + + // There might be already an equal address in the array even in case of + // RTM_NEWADDR message, e.g. when lifetime of IPv6 address is renewed. Equal + // in this case means that IP and prefix is the same but some attributes + // might be different. Remove existing equal address in case of RTM_DELADDR + // as well as RTM_NEWADDR message and add a new one in the latter case. + for (uint32_t i = 0; i < linkInfo->mAddresses.Length(); ++i) { + if (aNlh->nlmsg_type == RTM_NEWADDR && + linkInfo->mAddresses[i]->MsgEquals(*address)) { + // If the new address is exactly the same, there is nothing to do. + LOG(("Exactly the same address already exists [ifIdx=%u, addr=%s/%u", + ifIdx, addrStr.get(), address->GetPrefixLen())); + return; + } + + if (linkInfo->mAddresses[i]->Equals(*address)) { + LOG(("Removing address [ifIdx=%u, addr=%s/%u]", ifIdx, addrStr.get(), + address->GetPrefixLen())); + linkInfo->mAddresses.RemoveElementAt(i); + break; + } + } + + if (aNlh->nlmsg_type == RTM_NEWADDR) { + LOG(("Adding address [ifIdx=%u, addr=%s/%u]", ifIdx, addrStr.get(), + address->GetPrefixLen())); + linkInfo->mAddresses.AppendElement(std::move(address)); + } else { + // Remove all routes associated with this address + for (uint32_t i = linkInfo->mDefaultRoutes.Length(); i-- > 0;) { + MOZ_ASSERT(linkInfo->mDefaultRoutes[i]->GetGWAddrPtr(), + "Stored routes must have gateway!"); + if (linkInfo->mDefaultRoutes[i]->Family() == address->Family() && + address->ContainsAddr(linkInfo->mDefaultRoutes[i]->GetGWAddrPtr())) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + linkInfo->mDefaultRoutes[i]->GetAsString(routeDbgStr); + LOG(("Removing default route: %s", routeDbgStr.get())); + } + linkInfo->mDefaultRoutes.RemoveElementAt(i); + } + } + + // Remove all neighbors associated with this address + for (auto iter = linkInfo->mNeighbors.Iter(); !iter.Done(); iter.Next()) { + NetlinkNeighbor* neigh = iter.UserData(); + if (neigh->Family() == address->Family() && + address->ContainsAddr(neigh->GetAddrPtr())) { + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Removing neighbor %s", neighDbgStr.get())); + } + iter.Remove(); + } + } + } + + // Address change on the interface can change its status + linkInfo->UpdateStatus(); + + // Don't treat address changes during initial scan as a network change + if (mInitialScanFinished) { + // Send network event change regardless of whether the ID has changed or + // not + mSendNetworkChangeEvent = true; + TriggerNetworkIDCalculation(); + } +} + +void NetlinkService::OnRouteMessage(struct nlmsghdr* aNlh) { + LOG(("NetlinkService::OnRouteMessage [type=%s]", + aNlh->nlmsg_type == RTM_NEWROUTE ? "new" : "del")); + + UniquePtr<NetlinkRoute> route(new NetlinkRoute()); + if (!route->Init(aNlh)) { + return; + } + + if (!route->IsUnicast() || !route->ScopeIsUniverse()) { + // Use only unicast routes + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Not an unicast global route: %s", routeDbgStr.get())); + } + return; + } + + // Adding/removing any unicast route might change network ID + TriggerNetworkIDCalculation(); + + if (!route->IsDefault()) { + // Store only default routes + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Not a default route: %s", routeDbgStr.get())); + } + return; + } + + if (!route->HasOif()) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("There is no output interface in route: %s", routeDbgStr.get())); + } + return; + } + + if (!route->GetGWAddrPtr()) { + // We won't use the route if there is no gateway, so don't store it + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("There is no gateway in route: %s", routeDbgStr.get())); + } + return; + } + + if (route->Family() == AF_INET6 && + net::utils::ipv6_scope((const unsigned char*)route->GetGWAddrPtr()) != + IPV6_SCOPE_GLOBAL) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Scope of GW isn't global: %s", routeDbgStr.get())); + } + return; + } + + LinkInfo* linkInfo = nullptr; + mLinks.Get(route->Oif(), &linkInfo); + if (!linkInfo) { + // This can happen during startup + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Cannot find link info for route: %s", routeDbgStr.get())); + } + return; + } + + for (uint32_t i = 0; i < linkInfo->mDefaultRoutes.Length(); ++i) { + if (linkInfo->mDefaultRoutes[i]->Equals(*route)) { + // We shouldn't find equal route when adding a new one, but just in case + // it can happen remove the old one to avoid duplicities. + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Removing default route: %s", routeDbgStr.get())); + } + linkInfo->mDefaultRoutes.RemoveElementAt(i); + break; + } + } + + if (aNlh->nlmsg_type == RTM_NEWROUTE) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Adding default route: %s", routeDbgStr.get())); + } + linkInfo->mDefaultRoutes.AppendElement(std::move(route)); + } +} + +void NetlinkService::OnNeighborMessage(struct nlmsghdr* aNlh) { + LOG(("NetlinkService::OnNeighborMessage [type=%s]", + aNlh->nlmsg_type == RTM_NEWNEIGH ? "new" : "del")); + + UniquePtr<NetlinkNeighbor> neigh(new NetlinkNeighbor()); + if (!neigh->Init(aNlh)) { + return; + } + + LinkInfo* linkInfo = nullptr; + mLinks.Get(neigh->GetIndex(), &linkInfo); + if (!linkInfo) { + // This can happen during startup + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Cannot find link info for neighbor: %s", neighDbgStr.get())); + } + return; + } + + if (!linkInfo->mLink->IsTypeEther()) { + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Ignoring message on non-ethernet link: %s", neighDbgStr.get())); + } + return; + } + + nsAutoCString key; + GetAddrStr(neigh->GetAddrPtr(), neigh->Family(), key); + + if (aNlh->nlmsg_type == RTM_NEWNEIGH) { + if (!mRecalculateNetworkId && neigh->HasMAC()) { + NetlinkNeighbor* oldNeigh = nullptr; + linkInfo->mNeighbors.Get(key, &oldNeigh); + + if (!oldNeigh || !oldNeigh->HasMAC()) { + // The MAC address was added, if it's a host from some of the saved + // routing tables we should recalculate network ID + for (uint32_t i = 0; i < linkInfo->mDefaultRoutes.Length(); ++i) { + if (linkInfo->mDefaultRoutes[i]->GatewayEquals(*neigh)) { + TriggerNetworkIDCalculation(); + break; + } + } + if ((mIPv4RouteCheckResult && + mIPv4RouteCheckResult->GatewayEquals(*neigh)) || + (mIPv6RouteCheckResult && + mIPv6RouteCheckResult->GatewayEquals(*neigh))) { + TriggerNetworkIDCalculation(); + } + } + } + + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Adding neighbor: %s", neighDbgStr.get())); + } + linkInfo->mNeighbors.InsertOrUpdate(key, std::move(neigh)); + } else { + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Removing neighbor %s", neighDbgStr.get())); + } + linkInfo->mNeighbors.Remove(key); + } +} + +void NetlinkService::OnRouteCheckResult(struct nlmsghdr* aNlh) { + LOG(("NetlinkService::OnRouteCheckResult")); + UniquePtr<NetlinkRoute> route; + + if (aNlh) { + route = MakeUnique<NetlinkRoute>(); + if (!route->Init(aNlh)) { + route = nullptr; + } else { + if (!route->IsUnicast() || !route->ScopeIsUniverse()) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Not an unicast global route: %s", routeDbgStr.get())); + } + route = nullptr; + } else if (!route->HasOif()) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("There is no output interface in route: %s", routeDbgStr.get())); + } + route = nullptr; + } + } + } + + if (LOG_ENABLED()) { + if (route) { + nsAutoCString routeDbgStr; + route->GetAsString(routeDbgStr); + LOG(("Storing route: %s", routeDbgStr.get())); + } else { + LOG(("Clearing result for the check")); + } + } + + if (mOutgoingMessages[0]->Family() == AF_INET) { + mIPv4RouteCheckResult = std::move(route); + } else { + mIPv6RouteCheckResult = std::move(route); + } +} + +void NetlinkService::EnqueueGenMsg(uint16_t aMsgType, uint8_t aFamily) { + NetlinkGenMsg* msg = new NetlinkGenMsg(aMsgType, aFamily, ++mMsgId); + mOutgoingMessages.AppendElement(msg); +} + +void NetlinkService::EnqueueRtMsg(uint8_t aFamily, void* aAddress) { + NetlinkRtMsg* msg = new NetlinkRtMsg(aFamily, aAddress, ++mMsgId); + mOutgoingMessages.AppendElement(msg); +} + +void NetlinkService::RemovePendingMsg() { + LOG(("NetlinkService::RemovePendingMsg [seqId=%u]", + mOutgoingMessages[0]->SeqId())); + + MOZ_ASSERT(mOutgoingMessages[0]->IsPending()); + + DebugOnly<bool> isRtMessage = + (mOutgoingMessages[0]->MsgType() == NetlinkMsg::kRtMsg); + + mOutgoingMessages.RemoveElementAt(0); + if (!mOutgoingMessages.Length()) { + if (!mInitialScanFinished) { + // Now we've received all initial data from the kernel. Perform a link + // check and trigger network ID calculation even if it wasn't triggered + // by the incoming messages. + mInitialScanFinished = true; + + TriggerNetworkIDCalculation(); + + // Link status should be known by now. + RefPtr<NetlinkServiceListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + if (listener) { + listener->OnLinkStatusKnown(); + } + } else { + // We've received last response for route check, calculate ID now + MOZ_ASSERT(isRtMessage); + CalculateNetworkID(); + } + } +} + +NS_IMETHODIMP +NetlinkService::Run() { + int netlinkSocket = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (netlinkSocket < 0) { + return NS_ERROR_FAILURE; + } + + struct sockaddr_nl addr {}; + memset(&addr, 0, sizeof(addr)); + + addr.nl_family = AF_NETLINK; + addr.nl_groups = RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR | RTMGRP_LINK | + RTMGRP_NEIGH | RTMGRP_IPV4_ROUTE | RTMGRP_IPV6_ROUTE; + + if (bind(netlinkSocket, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + // failure! + EINTR_RETRY(close(netlinkSocket)); + return NS_ERROR_FAILURE; + } + + struct pollfd fds[2]; + fds[0].fd = mShutdownPipe[0]; + fds[0].events = POLLIN; + fds[0].revents = 0; + + fds[1].fd = netlinkSocket; + fds[1].events = POLLIN; + fds[1].revents = 0; + + // send all requests to get initial network information + EnqueueGenMsg(RTM_GETLINK, AF_PACKET); + EnqueueGenMsg(RTM_GETNEIGH, AF_INET); + EnqueueGenMsg(RTM_GETNEIGH, AF_INET6); + EnqueueGenMsg(RTM_GETADDR, AF_PACKET); + EnqueueGenMsg(RTM_GETROUTE, AF_PACKET); + + nsresult rv = NS_OK; + bool shutdown = false; + while (!shutdown) { + if (mOutgoingMessages.Length() && !mOutgoingMessages[0]->IsPending()) { + if (!mOutgoingMessages[0]->Send(netlinkSocket)) { + LOG(("Failed to send netlink message")); + mOutgoingMessages.RemoveElementAt(0); + // try to send another message if available before polling + continue; + } + } + + int rc = eintr_retry([&]() { + AUTO_PROFILER_THREAD_SLEEP; + return poll(fds, 2, GetPollWait()); + }); + + if (rc > 0) { + if (fds[0].revents & POLLIN) { + // shutdown, abort the loop! + LOG(("thread shutdown received, dying...\n")); + shutdown = true; + } else if (fds[1].revents & POLLIN) { + LOG(("netlink message received, handling it...\n")); + OnNetlinkMessage(netlinkSocket); + } + } else if (rc < 0) { + rv = NS_ERROR_FAILURE; + break; + } + } + + EINTR_RETRY(close(netlinkSocket)); + + return rv; +} + +nsresult NetlinkService::Init(NetlinkServiceListener* aListener) { + nsresult rv; + + mListener = aListener; + + 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 (inet_pton(AF_INET6, ROUTE_CHECK_IPV6, &mRouteCheckIPv6) != 1) { + LOG(("Cannot parse address " ROUTE_CHECK_IPV6)); + MOZ_DIAGNOSTIC_ASSERT(false, "Cannot parse address " ROUTE_CHECK_IPV6); + return NS_ERROR_UNEXPECTED; + } + + if (pipe(mShutdownPipe) == -1) { + LOG(("Cannot create pipe")); + return NS_ERROR_FAILURE; + } + + rv = NS_NewNamedThread("Netlink Monitor", getter_AddRefs(mThread), this); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult NetlinkService::Shutdown() { + LOG(("write() to signal thread shutdown\n")); + + { + MutexAutoLock lock(mMutex); + mListener = nullptr; + } + + // awake the thread to make it terminate + ssize_t rc = EINTR_RETRY(write(mShutdownPipe[1], "1", 1)); + LOG(("write() returned %d, errno == %d\n", (int)rc, errno)); + + nsresult rv = mThread->Shutdown(); + + // Have to break the cycle here, otherwise NetlinkService holds + // onto the thread and the thread holds onto the NetlinkService + // via its mRunnable + mThread = nullptr; + + return rv; +} + +/* + * A network event that might change network ID has been registered. Delay + * network ID calculation and sending of the event in case it changed for + * a while. Absorbing potential subsequent events increases chance of successful + * network ID calculation (e.g. MAC address of the router might be discovered in + * the meantime) + */ +void NetlinkService::TriggerNetworkIDCalculation() { + LOG(("NetlinkService::TriggerNetworkIDCalculation")); + + if (mRecalculateNetworkId) { + return; + } + + mRecalculateNetworkId = true; + mTriggerTime = TimeStamp::Now(); +} + +int NetlinkService::GetPollWait() { + if (!mRecalculateNetworkId) { + return -1; + } + + if (mOutgoingMessages.Length()) { + MOZ_ASSERT(mOutgoingMessages[0]->IsPending()); + // Message is pending, we don't have to set timeout because we'll receive + // reply from kernel ASAP + return -1; + } + + MOZ_ASSERT(mInitialScanFinished); + + double period = (TimeStamp::Now() - mTriggerTime).ToMilliseconds(); + if (period >= kNetworkChangeCoalescingPeriod) { + // Coalescing time has elapsed, send route check messages to find out + // where IPv4 and IPv6 traffic is routed and calculate network ID after + // the response is received. + EnqueueRtMsg(AF_INET, &mRouteCheckIPv4); + EnqueueRtMsg(AF_INET6, &mRouteCheckIPv6); + + // Return 0 to make sure we start sending enqueued messages immediately + return 0; + } + + return static_cast<int>(kNetworkChangeCoalescingPeriod - period); +} + +class NeighborComparator { + public: + bool Equals(const NetlinkNeighbor* a, const NetlinkNeighbor* b) const { + return (memcmp(a->GetMACPtr(), b->GetMACPtr(), ETH_ALEN) == 0); + } + bool LessThan(const NetlinkNeighbor* a, const NetlinkNeighbor* b) const { + return (memcmp(a->GetMACPtr(), b->GetMACPtr(), ETH_ALEN) < 0); + } +}; + +class LinknameComparator { + public: + bool LessThan(const nsCString& aA, const nsCString& aB) const { + return aA < aB; + } + bool Equals(const nsCString& aA, const nsCString& aB) const { + return aA == aB; + } +}; + +// Get Gateway Neighbours for a particular Address Family, for which we know MAC +// address +void NetlinkService::GetGWNeighboursForFamily( + uint8_t aFamily, nsTArray<NetlinkNeighbor*>& aGwNeighbors) { + LOG(("NetlinkService::GetGWNeighboursForFamily")); + // Check only routes on links that are up + for (const auto& linkInfo : mLinks.Values()) { + nsAutoCString linkName; + linkInfo->mLink->GetName(linkName); + + if (!linkInfo->mIsUp) { + LOG((" %s is down", linkName.get())); + continue; + } + + if (!linkInfo->mLink->IsTypeEther()) { + LOG((" %s is not ethernet link", linkName.get())); + continue; + } + + LOG((" checking link %s", linkName.get())); + + // Check all default routes and try to get MAC of the gateway + for (uint32_t i = 0; i < linkInfo->mDefaultRoutes.Length(); ++i) { + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + linkInfo->mDefaultRoutes[i]->GetAsString(routeDbgStr); + LOG(("Checking default route: %s", routeDbgStr.get())); + } + + if (linkInfo->mDefaultRoutes[i]->Family() != aFamily) { + LOG((" skipping due to different family")); + continue; + } + + MOZ_ASSERT(linkInfo->mDefaultRoutes[i]->GetGWAddrPtr(), + "Stored routes must have gateway!"); + + nsAutoCString neighKey; + GetAddrStr(linkInfo->mDefaultRoutes[i]->GetGWAddrPtr(), aFamily, + neighKey); + + NetlinkNeighbor* neigh = nullptr; + if (!linkInfo->mNeighbors.Get(neighKey, &neigh)) { + LOG(("Neighbor %s not found in hashtable.", neighKey.get())); + continue; + } + + if (!neigh->HasMAC()) { + // We don't know MAC address + LOG(("We have no MAC for neighbor %s.", neighKey.get())); + continue; + } + + if (aGwNeighbors.IndexOf(neigh, 0, NeighborComparator()) != + nsTArray<NetlinkNeighbor*>::NoIndex) { + // avoid host duplicities + LOG(("MAC of neighbor %s is already selected for hashing.", + neighKey.get())); + continue; + } + + LOG(("MAC of neighbor %s will be used for network ID.", neighKey.get())); + aGwNeighbors.AppendElement(neigh); + } + } +} + +bool NetlinkService::CalculateIDForEthernetLink(uint8_t aFamily, + NetlinkRoute* aRouteCheckResult, + uint32_t aRouteCheckIfIdx, + LinkInfo* aRouteCheckLinkInfo, + SHA1Sum* aSHA1) { + LOG(("NetlinkService::CalculateIDForEthernetLink")); + bool retval = false; + const in_common_addr* addrPtr = aRouteCheckResult->GetGWAddrPtr(); + + if (!addrPtr) { + // This shouldn't normally happen, missing next hop in case of ethernet + // device would mean that the checked host is on the same network. + if (LOG_ENABLED()) { + nsAutoCString routeDbgStr; + aRouteCheckResult->GetAsString(routeDbgStr); + LOG(("There is no next hop in route: %s", routeDbgStr.get())); + } + return retval; + } + + // If we know MAC address of the next hop for mRouteCheckIPv4/6 host, hash + // it even if it's MAC of some of the default routes we've checked above. + // This ensures that if we have 2 different default routes and next hop for + // mRouteCheckIPv4/6 changes from one default route to the other, we'll + // detect it as a network change. + nsAutoCString neighKey; + GetAddrStr(addrPtr, aFamily, neighKey); + LOG(("Next hop for the checked host is %s on ifIdx %u.", neighKey.get(), + aRouteCheckIfIdx)); + + NetlinkNeighbor* neigh = nullptr; + if (!aRouteCheckLinkInfo->mNeighbors.Get(neighKey, &neigh)) { + LOG(("Neighbor %s not found in hashtable.", neighKey.get())); + return retval; + } + + if (!neigh->HasMAC()) { + LOG(("We have no MAC for neighbor %s.", neighKey.get())); + return retval; + } + + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + neigh->GetAsString(neighDbgStr); + LOG(("Hashing MAC address of neighbor: %s", neighDbgStr.get())); + } + aSHA1->update(neigh->GetMACPtr(), ETH_ALEN); + retval = true; + + return retval; +} + +bool NetlinkService::CalculateIDForNonEthernetLink( + uint8_t aFamily, NetlinkRoute* aRouteCheckResult, + nsTArray<nsCString>& aLinkNamesToHash, uint32_t aRouteCheckIfIdx, + LinkInfo* aRouteCheckLinkInfo, SHA1Sum* aSHA1) { + LOG(("NetlinkService::CalculateIDForNonEthernetLink")); + bool retval = false; + const in_common_addr* addrPtr = aRouteCheckResult->GetGWAddrPtr(); + nsAutoCString routeCheckLinkName; + aRouteCheckLinkInfo->mLink->GetName(routeCheckLinkName); + + if (addrPtr) { + // The route contains next hop. Hash the name of the interface (e.g. + // "tun1") and the IP address of the next hop. + + nsAutoCString addrStr; + GetAddrStr(addrPtr, aFamily, addrStr); + size_t addrSize = + (aFamily == AF_INET) ? sizeof(addrPtr->addr4) : sizeof(addrPtr->addr6); + + LOG(("Hashing link name %s", routeCheckLinkName.get())); + aSHA1->update(routeCheckLinkName.get(), routeCheckLinkName.Length()); + + // Don't hash GW address if it's rmnet_data device. + if (!aLinkNamesToHash.Contains(routeCheckLinkName)) { + LOG(("Hashing GW address %s", addrStr.get())); + aSHA1->update(addrPtr, addrSize); + } + + retval = true; + } else { + // The traffic is routed directly via an interface. Hash the name of the + // interface and the network address. Using host address would cause that + // network ID would be different every time we get a different IP address + // in this network/VPN. + + bool hasSrcAddr = aRouteCheckResult->HasPrefSrcAddr(); + if (!hasSrcAddr) { + LOG(("There is no preferred source address.")); + } + + NetlinkAddress* linkAddress = nullptr; + // Find network address of the interface matching the source address. In + // theory there could be multiple addresses with different prefix length. + // Get the one with smallest prefix length. + for (uint32_t i = 0; i < aRouteCheckLinkInfo->mAddresses.Length(); ++i) { + if (!hasSrcAddr) { + // there is no preferred src, match just the family + if (aRouteCheckLinkInfo->mAddresses[i]->Family() != aFamily) { + continue; + } + } else if (!aRouteCheckResult->PrefSrcAddrEquals( + *aRouteCheckLinkInfo->mAddresses[i])) { + continue; + } + + if (!linkAddress || + linkAddress->GetPrefixLen() > + aRouteCheckLinkInfo->mAddresses[i]->GetPrefixLen()) { + // We have no address yet or this one has smaller prefix length, + // use it. + linkAddress = aRouteCheckLinkInfo->mAddresses[i].get(); + } + } + + if (!linkAddress) { + // There is no address in our array? + if (LOG_ENABLED()) { + nsAutoCString dbgStr; + aRouteCheckResult->GetAsString(dbgStr); + LOG(("No address found for preferred source address in route: %s", + dbgStr.get())); + } + return retval; + } + + in_common_addr prefix; + int32_t prefixSize = (aFamily == AF_INET) ? (int32_t)sizeof(prefix.addr4) + : (int32_t)sizeof(prefix.addr6); + memcpy(&prefix, linkAddress->GetAddrPtr(), prefixSize); + uint8_t maskit[] = {0x00, 0x80, 0xc0, 0xe0, 0xf0, 0xf8, 0xfc, 0xfe}; + int32_t bits = linkAddress->GetPrefixLen(); + if (bits > prefixSize * 8) { + MOZ_ASSERT(false, "Unexpected prefix length!"); + LOG(("Unexpected prefix length %d, maximum for this family is %d", bits, + prefixSize * 8)); + return retval; + } + for (int32_t i = 0; i < prefixSize; i++) { + uint8_t mask = (bits >= 8) ? 0xff : maskit[bits]; + ((unsigned char*)&prefix)[i] &= mask; + bits -= 8; + if (bits <= 0) { + bits = 0; + } + } + + nsAutoCString addrStr; + GetAddrStr(&prefix, aFamily, addrStr); + LOG(("Hashing link name %s and network address %s/%u", + routeCheckLinkName.get(), addrStr.get(), linkAddress->GetPrefixLen())); + aSHA1->update(routeCheckLinkName.get(), routeCheckLinkName.Length()); + aSHA1->update(&prefix, prefixSize); + bits = linkAddress->GetPrefixLen(); + aSHA1->update(&bits, sizeof(bits)); + retval = true; + } + return retval; +} + +bool NetlinkService::CalculateIDForFamily(uint8_t aFamily, SHA1Sum* aSHA1) { + LOG(("NetlinkService::CalculateIDForFamily [family=%s]", + aFamily == AF_INET ? "AF_INET" : "AF_INET6")); + + bool retval = false; + + if (!mLinkUp) { + // Skip ID calculation if the link is down, we have no ID... + LOG(("Link is down, skipping ID calculation.")); + return retval; + } + + NetlinkRoute* routeCheckResult; + if (aFamily == AF_INET) { + routeCheckResult = mIPv4RouteCheckResult.get(); + } else { + routeCheckResult = mIPv6RouteCheckResult.get(); + } + + // All GW neighbors for which we know MAC address. We'll probably have at + // most only one, but in case we have more default routes, we hash them all + // even though the routing rules sends the traffic only via one of them. + // If the system switches between them, we'll detect the change with + // mIPv4/6RouteCheckResult. + nsTArray<NetlinkNeighbor*> gwNeighbors; + + GetGWNeighboursForFamily(aFamily, gwNeighbors); + + // Sort them so we always have the same network ID on the same network + gwNeighbors.Sort(NeighborComparator()); + + for (uint32_t i = 0; i < gwNeighbors.Length(); ++i) { + if (LOG_ENABLED()) { + nsAutoCString neighDbgStr; + gwNeighbors[i]->GetAsString(neighDbgStr); + LOG(("Hashing MAC address of neighbor: %s", neighDbgStr.get())); + } + aSHA1->update(gwNeighbors[i]->GetMACPtr(), ETH_ALEN); + retval = true; + } + + nsTArray<nsCString> linkNamesToHash; + if (!gwNeighbors.Length()) { + // If we don't know MAC of the gateway and link is up, it's probably not + // an ethernet link. If the name of the link begins with "rmnet" then + // the mobile data is used. We cannot easily differentiate when user + // switches sim cards so let's treat mobile data as a single network. We'll + // simply hash link name. If the traffic is redirected via some VPN, it'll + // still be detected below. + + // TODO: maybe we could get operator name via AndroidBridge + for (const auto& linkInfo : mLinks.Values()) { + if (linkInfo->mIsUp) { + nsAutoCString linkName; + linkInfo->mLink->GetName(linkName); + if (StringBeginsWith(linkName, "rmnet"_ns)) { + // Check whether there is some non-local address associated with this + // link. + for (uint32_t i = 0; i < linkInfo->mAddresses.Length(); ++i) { + if (linkInfo->mAddresses[i]->Family() == aFamily && + linkInfo->mAddresses[i]->ScopeIsUniverse()) { + linkNamesToHash.AppendElement(linkName); + break; + } + } + } + } + } + + // Sort link names to ensure consistent results + linkNamesToHash.Sort(LinknameComparator()); + + for (uint32_t i = 0; i < linkNamesToHash.Length(); ++i) { + LOG(("Hashing name of adapter: %s", linkNamesToHash[i].get())); + aSHA1->update(linkNamesToHash[i].get(), linkNamesToHash[i].Length()); + retval = true; + } + } + + if (!routeCheckResult) { + // If we don't have result for route check to mRouteCheckIPv4/6 host, the + // network is unreachable and there is no more to do. + LOG(("There is no route check result.")); + return retval; + } + + LinkInfo* routeCheckLinkInfo = nullptr; + uint32_t routeCheckIfIdx = routeCheckResult->Oif(); + if (!mLinks.Get(routeCheckIfIdx, &routeCheckLinkInfo)) { + LOG(("Cannot find link with index %u ??", routeCheckIfIdx)); + return retval; + } + + if (routeCheckLinkInfo->mLink->IsTypeEther()) { + // The traffic is routed through an ethernet device. + retval |= CalculateIDForEthernetLink( + aFamily, routeCheckResult, routeCheckIfIdx, routeCheckLinkInfo, aSHA1); + } else { + // The traffic is routed through a non-ethernet device. + retval |= CalculateIDForNonEthernetLink(aFamily, routeCheckResult, + linkNamesToHash, routeCheckIfIdx, + routeCheckLinkInfo, aSHA1); + } + + return retval; +} + +void NetlinkService::ExtractDNSProperties() { + MOZ_ASSERT(!NS_IsMainThread(), "Must not be called on the main thread"); + nsTArray<nsCString> suffixList; + nsTArray<NetAddr> resolvers; +#if defined(HAVE_RES_NINIT) + [&]() { + struct __res_state res {}; + int ret = res_ninit(&res); + if (ret != 0) { + LOG(("Call to res_ninit failed: %d", ret)); + return; + } + + // Get DNS suffixes + for (int i = 0; i < MAXDNSRCH; i++) { + if (!res.dnsrch[i]) { + break; + } + suffixList.AppendElement(nsCString(res.dnsrch[i])); + } + + // Get DNS resolvers + // Chromium's dns_config_service_posix.cc is the origin of this code + // Initially, glibc stores IPv6 in |_ext.nsaddrs| and IPv4 in |nsaddr_list|. + // In res_send.c:res_nsend, it merges |nsaddr_list| into |nsaddrs|, + // but we have to combine the two arrays ourselves. + for (int i = 0; i < res.nscount; ++i) { + const struct sockaddr* addr = nullptr; + size_t addr_len = 0; + if (res.nsaddr_list[i].sin_family) { // The indicator used by res_nsend. + addr = reinterpret_cast<const struct sockaddr*>(&res.nsaddr_list[i]); + addr_len = sizeof res.nsaddr_list[i]; + } else if (res._u._ext.nsaddrs[i]) { + addr = reinterpret_cast<const struct sockaddr*>(res._u._ext.nsaddrs[i]); + addr_len = sizeof *res._u._ext.nsaddrs[i]; + } else { + LOG(("Bad ext struct")); + return; + } + const socklen_t kSockaddrInSize = sizeof(struct sockaddr_in); + const socklen_t kSockaddrIn6Size = sizeof(struct sockaddr_in6); + + if ((addr->sa_family == AF_INET && addr_len < kSockaddrInSize) || + (addr->sa_family == AF_INET6 && addr_len < kSockaddrIn6Size)) { + LOG(("Bad address size")); + return; + } + + NetAddr ip; + if (addr->sa_family == AF_INET) { + const struct sockaddr_in* sin = (const struct sockaddr_in*)addr; + ip.inet.family = AF_INET; + ip.inet.ip = sin->sin_addr.s_addr; + ip.inet.port = sin->sin_port; + } else if (addr->sa_family == AF_INET6) { + const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)addr; + ip.inet6.family = AF_INET6; + memcpy(&ip.inet6.ip.u8, &sin6->sin6_addr, sizeof(ip.inet6.ip.u8)); + ip.inet6.port = sin6->sin6_port; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected sa_family"); + return; + } + + resolvers.AppendElement(ip); + } + + res_nclose(&res); + }(); + +#endif + RefPtr<NetlinkServiceListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + mDNSSuffixList = std::move(suffixList); + mDNSResolvers = std::move(resolvers); + } + + if (listener) { + listener->OnDnsSuffixListUpdated(); + } +} + +void NetlinkService::UpdateLinkStatus() { + LOG(("NetlinkService::UpdateLinkStatus")); + + MOZ_ASSERT(!mRecalculateNetworkId); + MOZ_ASSERT(mInitialScanFinished); + + // Link is up when we have a route for ROUTE_CHECK_IPV4 or ROUTE_CHECK_IPV6 + bool newLinkUp = mIPv4RouteCheckResult || mIPv6RouteCheckResult; + + if (mLinkUp == newLinkUp) { + LOG(("Link status hasn't changed [linkUp=%d]", mLinkUp)); + } else { + LOG(("Link status has changed [linkUp=%d]", newLinkUp)); + RefPtr<NetlinkServiceListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + mLinkUp = newLinkUp; + } + if (mLinkUp) { + if (listener) { + listener->OnLinkUp(); + } + } else { + if (listener) { + listener->OnLinkDown(); + } + } + } +} + +// Figure out the "network identification". +void NetlinkService::CalculateNetworkID() { + LOG(("NetlinkService::CalculateNetworkID")); + + MOZ_ASSERT(!NS_IsMainThread(), "Must not be called on the main thread"); + MOZ_ASSERT(mRecalculateNetworkId); + + mRecalculateNetworkId = false; + + SHA1Sum sha1; + + UpdateLinkStatus(); + ExtractDNSProperties(); + + bool idChanged = false; + bool found4 = CalculateIDForFamily(AF_INET, &sha1); + bool found6 = CalculateIDForFamily(AF_INET6, &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); + } + } + + // If this is first time we calculate network ID, don't report it as a network + // change. We've started with an empty ID and we've just calculated the + // correct ID. The network hasn't really changed. + static bool initialIDCalculation = true; + + RefPtr<NetlinkServiceListener> listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + } + + if (!initialIDCalculation && idChanged && listener) { + listener->OnNetworkIDChanged(); + mSendNetworkChangeEvent = true; + } + + if (mSendNetworkChangeEvent && listener) { + listener->OnNetworkChanged(); + } + + initialIDCalculation = false; + mSendNetworkChangeEvent = false; +} + +void NetlinkService::GetNetworkID(nsACString& aNetworkID) { + MutexAutoLock lock(mMutex); + aNetworkID = mNetworkId; +} + +nsresult NetlinkService::GetDnsSuffixList(nsTArray<nsCString>& aDnsSuffixList) { +#if defined(HAVE_RES_NINIT) + MutexAutoLock lock(mMutex); + aDnsSuffixList = mDNSSuffixList.Clone(); + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +nsresult NetlinkService::GetResolvers(nsTArray<NetAddr>& aResolvers) { +#if defined(HAVE_RES_NINIT) + MutexAutoLock lock(mMutex); + aResolvers = mDNSResolvers.Clone(); + return NS_OK; +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif +} + +void NetlinkService::GetIsLinkUp(bool* aIsUp) { + MutexAutoLock lock(mMutex); + *aIsUp = mLinkUp; +} + +} // namespace mozilla::net |