Mypal/netwerk/system/win32/nsNotifyAddrListener.cpp

740 lines
23 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* vim:set et sw=4 ts=4: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// We define this to make our use of inet_ntoa() pass. The "proper" function
// inet_ntop() doesn't exist on Windows XP.
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdarg.h>
#include <windef.h>
#include <winbase.h>
#include <wingdi.h>
#include <winuser.h>
#include <ole2.h>
#include <netcon.h>
#include <objbase.h>
#include <winsock2.h>
#include <ws2ipdef.h>
#include <tcpmib.h>
#include <iphlpapi.h>
#include <netioapi.h>
#include <iprtrmib.h>
#include "plstr.h"
#include "mozilla/Logging.h"
#include "nsThreadUtils.h"
#include "nsIObserverService.h"
#include "nsServiceManagerUtils.h"
#include "nsNotifyAddrListener.h"
#include "nsString.h"
#include "nsAutoPtr.h"
#include "mozilla/Services.h"
#include "nsCRT.h"
#include "mozilla/Preferences.h"
#include "mozilla/SHA1.h"
#include "mozilla/Base64.h"
#include <iptypes.h>
#include <iphlpapi.h>
using namespace mozilla;
static LazyLogModule gNotifyAddrLog("nsNotifyAddr");
#define LOG(args) MOZ_LOG(gNotifyAddrLog, mozilla::LogLevel::Debug, args)
static HMODULE sNetshell;
static decltype(NcFreeNetconProperties)* sNcFreeNetconProperties;
static HMODULE sIphlpapi;
static decltype(NotifyIpInterfaceChange)* sNotifyIpInterfaceChange;
static decltype(CancelMibChangeNotify2)* sCancelMibChangeNotify2;
#define NETWORK_NOTIFY_CHANGED_PREF "network.notify.changed"
#define NETWORK_NOTIFY_IPV6_PREF "network.notify.IPv6"
// period during which to absorb subsequent network change events, in
// milliseconds
static const unsigned int kNetworkChangeCoalescingPeriod = 1000;
static void InitIphlpapi(void)
{
if (!sIphlpapi) {
sIphlpapi = LoadLibraryW(L"Iphlpapi.dll");
if (sIphlpapi) {
sNotifyIpInterfaceChange = (decltype(NotifyIpInterfaceChange)*)
GetProcAddress(sIphlpapi, "NotifyIpInterfaceChange");
sCancelMibChangeNotify2 = (decltype(CancelMibChangeNotify2)*)
GetProcAddress(sIphlpapi, "CancelMibChangeNotify2");
} else {
NS_WARNING("Failed to load Iphlpapi.dll - cannot detect network"
" changes!");
}
}
}
static void InitNetshellLibrary(void)
{
if (!sNetshell) {
sNetshell = LoadLibraryW(L"Netshell.dll");
if (sNetshell) {
sNcFreeNetconProperties = (decltype(NcFreeNetconProperties)*)
GetProcAddress(sNetshell, "NcFreeNetconProperties");
}
}
}
static void FreeDynamicLibraries(void)
{
if (sNetshell) {
sNcFreeNetconProperties = nullptr;
FreeLibrary(sNetshell);
sNetshell = nullptr;
}
if (sIphlpapi) {
sNotifyIpInterfaceChange = nullptr;
sCancelMibChangeNotify2 = nullptr;
FreeLibrary(sIphlpapi);
sIphlpapi = nullptr;
}
}
NS_IMPL_ISUPPORTS(nsNotifyAddrListener,
nsINetworkLinkService,
nsIRunnable,
nsIObserver)
nsNotifyAddrListener::nsNotifyAddrListener()
: mLinkUp(true) // assume true by default
, mStatusKnown(false)
, mCheckAttempted(false)
, mCheckEvent(nullptr)
, mShutdown(false)
, mIPInterfaceChecksum(0)
, mAllowChangedEvent(true)
, mIPv6Changes(false)
, mCoalescingActive(false)
{
InitIphlpapi();
}
nsNotifyAddrListener::~nsNotifyAddrListener()
{
NS_ASSERTION(!mThread, "nsNotifyAddrListener thread shutdown failed");
FreeDynamicLibraries();
}
NS_IMETHODIMP
nsNotifyAddrListener::GetIsLinkUp(bool *aIsUp)
{
if (!mCheckAttempted && !mStatusKnown) {
mCheckAttempted = true;
CheckLinkStatus();
}
*aIsUp = mLinkUp;
return NS_OK;
}
NS_IMETHODIMP
nsNotifyAddrListener::GetLinkStatusKnown(bool *aIsUp)
{
*aIsUp = mStatusKnown;
return NS_OK;
}
NS_IMETHODIMP
nsNotifyAddrListener::GetLinkType(uint32_t *aLinkType)
{
NS_ENSURE_ARG_POINTER(aLinkType);
// XXX This function has not yet been implemented for this platform
*aLinkType = nsINetworkLinkService::LINK_TYPE_UNKNOWN;
return NS_OK;
}
static bool macAddr(BYTE addr[], DWORD len, char *buf, size_t buflen)
{
buf[0] = '\0';
if (!addr || !len || (len * 3 > buflen)) {
return false;
}
for (DWORD i = 0; i < len; ++i) {
sprintf_s(buf + (i * 3), sizeof(buf + (i * 3)),
"%02x%s", addr[i], (i == len-1) ? "" : ":");
}
return true;
}
bool nsNotifyAddrListener::findMac(char *gateway)
{
// query for buffer size needed
DWORD dwActualSize = 0;
bool found = FALSE;
// GetIpNetTable gets the IPv4 to physical address mapping table
DWORD status = GetIpNetTable(NULL, &dwActualSize, FALSE);
if (status == ERROR_INSUFFICIENT_BUFFER) {
// the expected route, now with a known buffer size
UniquePtr <char[]>buf(new char[dwActualSize]);
PMIB_IPNETTABLE pIpNetTable =
reinterpret_cast<PMIB_IPNETTABLE>(&buf[0]);
status = GetIpNetTable(pIpNetTable, &dwActualSize, FALSE);
if (status == NO_ERROR) {
for (DWORD i = 0; i < pIpNetTable->dwNumEntries; ++i) {
DWORD dwCurrIndex = pIpNetTable->table[i].dwIndex;
char hw[256];
if (!macAddr(pIpNetTable->table[i].bPhysAddr,
pIpNetTable->table[i].dwPhysAddrLen,
hw, sizeof(hw))) {
// failed to get the MAC
continue;
}
struct in_addr addr;
addr.s_addr = pIpNetTable->table[i].dwAddr;
if (!strcmp(gateway, inet_ntoa(addr))) {
LOG(("networkid: MAC %s\n", hw));
nsAutoCString mac(hw);
// This 'addition' could potentially be a
// fixed number from the profile or something.
nsAutoCString addition("local-rubbish");
nsAutoCString output;
SHA1Sum sha1;
nsCString combined(mac + addition);
sha1.update(combined.get(), combined.Length());
uint8_t digest[SHA1Sum::kHashSize];
sha1.finish(digest);
nsCString newString(reinterpret_cast<char*>(digest),
SHA1Sum::kHashSize);
Base64Encode(newString, output);
LOG(("networkid: id %s\n", output.get()));
if (mNetworkId != output) {
// new id
mNetworkId = output;
}
else {
// same id
}
found = true;
break;
}
}
}
}
return found;
}
// returns 'true' when the gw is found and stored
static bool defaultgw(char *aGateway, size_t aGatewayLen)
{
PMIB_IPFORWARDTABLE pIpForwardTable = NULL;
DWORD dwSize = 0;
if (GetIpForwardTable(NULL, &dwSize, 0) != ERROR_INSUFFICIENT_BUFFER) {
return false;
}
UniquePtr <char[]>buf(new char[dwSize]);
pIpForwardTable = reinterpret_cast<PMIB_IPFORWARDTABLE>(&buf[0]);
// Note that the IPv4 addresses returned in GetIpForwardTable entries are
// in network byte order
DWORD retVal = GetIpForwardTable(pIpForwardTable, &dwSize, 0);
if (retVal == NO_ERROR) {
for (unsigned int i = 0; i < pIpForwardTable->dwNumEntries; ++i) {
// Convert IPv4 addresses to strings
struct in_addr IpAddr;
IpAddr.S_un.S_addr = static_cast<u_long>
(pIpForwardTable->table[i].dwForwardDest);
char *ipStr = inet_ntoa(IpAddr);
if (ipStr && !strcmp("0.0.0.0", ipStr)) {
// Default gateway!
IpAddr.S_un.S_addr = static_cast<u_long>
(pIpForwardTable->table[i].dwForwardNextHop);
ipStr = inet_ntoa(IpAddr);
if (ipStr) {
strcpy_s(aGateway, aGatewayLen, ipStr);
return true;
}
}
} // for loop
}
return false;
}
//
// Figure out the current "network identification" string.
//
// It detects the IP of the default gateway in the routing table, then the MAC
// address of that IP in the ARP table before it hashes that string (to avoid
// information leakage).
//
void nsNotifyAddrListener::calculateNetworkId(void)
{
bool found = FALSE;
char gateway[128];
if (defaultgw(gateway, sizeof(gateway) )) {
found = findMac(gateway);
}
if (!found) {
// no id
}
}
// Static Callback function for NotifyIpInterfaceChange API.
static void WINAPI OnInterfaceChange(PVOID callerContext,
PMIB_IPINTERFACE_ROW row,
MIB_NOTIFICATION_TYPE notificationType)
{
nsNotifyAddrListener *notify = static_cast<nsNotifyAddrListener*>(callerContext);
notify->CheckLinkStatus();
}
DWORD
nsNotifyAddrListener::nextCoalesceWaitTime()
{
// check if coalescing period should continue
double period = (TimeStamp::Now() - mChangeTime).ToMilliseconds();
if (period >= kNetworkChangeCoalescingPeriod) {
calculateNetworkId();
SendEvent(NS_NETWORK_LINK_DATA_CHANGED);
mCoalescingActive = false;
return INFINITE; // return default
} else {
// wait no longer than to the end of the period
return static_cast<DWORD>
(kNetworkChangeCoalescingPeriod - period);
}
}
NS_IMETHODIMP
nsNotifyAddrListener::Run()
{
PR_SetCurrentThreadName("Link Monitor");
mStartTime = TimeStamp::Now();
calculateNetworkId();
DWORD waitTime = INFINITE;
if (!sNotifyIpInterfaceChange || !sCancelMibChangeNotify2 || !mIPv6Changes) {
// For Windows versions which are older than Vista which lack
// NotifyIpInterfaceChange. Note this means no IPv6 support.
HANDLE ev = CreateEvent(nullptr, FALSE, FALSE, nullptr);
NS_ENSURE_TRUE(ev, NS_ERROR_OUT_OF_MEMORY);
HANDLE handles[2] = { ev, mCheckEvent };
OVERLAPPED overlapped = { 0 };
bool shuttingDown = false;
overlapped.hEvent = ev;
while (!shuttingDown) {
HANDLE h;
DWORD ret = NotifyAddrChange(&h, &overlapped);
if (ret == ERROR_IO_PENDING) {
ret = WaitForMultipleObjects(2, handles, FALSE, waitTime);
if (ret == WAIT_OBJECT_0) {
CheckLinkStatus();
} else if (!mShutdown) {
waitTime = nextCoalesceWaitTime();
} else {
shuttingDown = true;
}
} else {
shuttingDown = true;
}
}
CloseHandle(ev);
} else {
// Windows Vista and newer versions.
HANDLE interfacechange;
// The callback will simply invoke CheckLinkStatus()
DWORD ret = sNotifyIpInterfaceChange(
AF_UNSPEC, // IPv4 and IPv6
(PIPINTERFACE_CHANGE_CALLBACK)OnInterfaceChange,
this, // pass to callback
false, // no initial notification
&interfacechange);
if (ret == NO_ERROR) {
do {
ret = WaitForSingleObject(mCheckEvent, waitTime);
if (!mShutdown) {
waitTime = nextCoalesceWaitTime();
}
else {
break;
}
} while (ret != WAIT_FAILED);
sCancelMibChangeNotify2(interfacechange);
} else {
LOG(("Link Monitor: sNotifyIpInterfaceChange returned %d\n",
(int)ret));
}
}
return NS_OK;
}
NS_IMETHODIMP
nsNotifyAddrListener::Observe(nsISupports *subject,
const char *topic,
const char16_t *data)
{
if (!strcmp("xpcom-shutdown-threads", topic))
Shutdown();
return NS_OK;
}
nsresult
nsNotifyAddrListener::Init(void)
{
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (!observerService)
return NS_ERROR_FAILURE;
nsresult rv = observerService->AddObserver(this, "xpcom-shutdown-threads",
false);
NS_ENSURE_SUCCESS(rv, rv);
Preferences::AddBoolVarCache(&mAllowChangedEvent,
NETWORK_NOTIFY_CHANGED_PREF, true);
Preferences::AddBoolVarCache(&mIPv6Changes,
NETWORK_NOTIFY_IPV6_PREF, false);
mCheckEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
NS_ENSURE_TRUE(mCheckEvent, NS_ERROR_OUT_OF_MEMORY);
rv = NS_NewThread(getter_AddRefs(mThread), this);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
nsNotifyAddrListener::Shutdown(void)
{
// remove xpcom shutdown observer
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService)
observerService->RemoveObserver(this, "xpcom-shutdown-threads");
if (!mCheckEvent)
return NS_OK;
mShutdown = true;
SetEvent(mCheckEvent);
nsresult rv = mThread ? mThread->Shutdown() : NS_OK;
// Have to break the cycle here, otherwise nsNotifyAddrListener holds
// onto the thread and the thread holds onto the nsNotifyAddrListener
// via its mRunnable
mThread = nullptr;
CloseHandle(mCheckEvent);
mCheckEvent = nullptr;
return rv;
}
/*
* A network event has been registered. Delay the actual sending of the event
* for a while and absorb subsequent events in the mean time in an effort to
* squash potentially many triggers into a single event.
* Only ever called from the same thread.
*/
nsresult
nsNotifyAddrListener::NetworkChanged()
{
if (mCoalescingActive) {
LOG(("NetworkChanged: absorbed an event (coalescing active)\n"));
} else {
// A fresh trigger!
mChangeTime = TimeStamp::Now();
mCoalescingActive = true;
SetEvent(mCheckEvent);
LOG(("NetworkChanged: coalescing period started\n"));
}
return NS_OK;
}
/* Sends the given event. Assumes aEventID never goes out of scope (static
* strings are ideal).
*/
nsresult
nsNotifyAddrListener::SendEvent(const char *aEventID)
{
if (!aEventID)
return NS_ERROR_NULL_POINTER;
LOG(("SendEvent: network is '%s'\n", aEventID));
nsresult rv;
nsCOMPtr<nsIRunnable> event = new ChangeEvent(this, aEventID);
if (NS_FAILED(rv = NS_DispatchToMainThread(event)))
NS_WARNING("Failed to dispatch ChangeEvent");
return rv;
}
NS_IMETHODIMP
nsNotifyAddrListener::ChangeEvent::Run()
{
nsCOMPtr<nsIObserverService> observerService =
mozilla::services::GetObserverService();
if (observerService)
observerService->NotifyObservers(
mService, NS_NETWORK_LINK_TOPIC,
NS_ConvertASCIItoUTF16(mEventID).get());
return NS_OK;
}
// Bug 465158 features an explanation for this check. ICS being "Internet
// Connection Sharing). The description says it is always IP address
// 192.168.0.1 for this case.
bool
nsNotifyAddrListener::CheckICSGateway(PIP_ADAPTER_ADDRESSES aAdapter)
{
if (!aAdapter->FirstUnicastAddress)
return false;
LPSOCKADDR aAddress = aAdapter->FirstUnicastAddress->Address.lpSockaddr;
if (!aAddress)
return false;
PSOCKADDR_IN in_addr = (PSOCKADDR_IN)aAddress;
bool isGateway = (aAddress->sa_family == AF_INET &&
in_addr->sin_addr.S_un.S_un_b.s_b1 == 192 &&
in_addr->sin_addr.S_un.S_un_b.s_b2 == 168 &&
in_addr->sin_addr.S_un.S_un_b.s_b3 == 0 &&
in_addr->sin_addr.S_un.S_un_b.s_b4 == 1);
if (isGateway)
isGateway = CheckICSStatus(aAdapter->FriendlyName);
return isGateway;
}
bool
nsNotifyAddrListener::CheckICSStatus(PWCHAR aAdapterName)
{
InitNetshellLibrary();
// This method enumerates all privately shared connections and checks if some
// of them has the same name as the one provided in aAdapterName. If such
// connection is found in the collection the adapter is used as ICS gateway
bool isICSGatewayAdapter = false;
HRESULT hr;
RefPtr<INetSharingManager> netSharingManager;
hr = CoCreateInstance(
CLSID_NetSharingManager,
nullptr,
CLSCTX_INPROC_SERVER,
IID_INetSharingManager,
getter_AddRefs(netSharingManager));
RefPtr<INetSharingPrivateConnectionCollection> privateCollection;
if (SUCCEEDED(hr)) {
hr = netSharingManager->get_EnumPrivateConnections(
ICSSC_DEFAULT,
getter_AddRefs(privateCollection));
}
RefPtr<IEnumNetSharingPrivateConnection> privateEnum;
if (SUCCEEDED(hr)) {
RefPtr<IUnknown> privateEnumUnknown;
hr = privateCollection->get__NewEnum(getter_AddRefs(privateEnumUnknown));
if (SUCCEEDED(hr)) {
hr = privateEnumUnknown->QueryInterface(
IID_IEnumNetSharingPrivateConnection,
getter_AddRefs(privateEnum));
}
}
if (SUCCEEDED(hr)) {
ULONG fetched;
VARIANT connectionVariant;
while (!isICSGatewayAdapter &&
SUCCEEDED(hr = privateEnum->Next(1, &connectionVariant,
&fetched)) &&
fetched) {
if (connectionVariant.vt != VT_UNKNOWN) {
// We should call VariantClear here but it needs to link
// with oleaut32.lib that produces a Ts incrase about 10ms
// that is undesired. As it is quit unlikely the result would
// be of a different type anyway, let's pass the variant
// unfreed here.
NS_ERROR("Variant of unexpected type, expecting VT_UNKNOWN, we probably leak it!");
continue;
}
RefPtr<INetConnection> connection;
if (SUCCEEDED(connectionVariant.punkVal->QueryInterface(
IID_INetConnection,
getter_AddRefs(connection)))) {
connectionVariant.punkVal->Release();
NETCON_PROPERTIES *properties;
if (SUCCEEDED(connection->GetProperties(&properties))) {
if (!wcscmp(properties->pszwName, aAdapterName))
isICSGatewayAdapter = true;
if (sNcFreeNetconProperties)
sNcFreeNetconProperties(properties);
}
}
}
}
return isICSGatewayAdapter;
}
DWORD
nsNotifyAddrListener::CheckAdaptersAddresses(void)
{
ULONG len = 16384;
PIP_ADAPTER_ADDRESSES adapterList = (PIP_ADAPTER_ADDRESSES) moz_xmalloc(len);
ULONG flags = GAA_FLAG_SKIP_DNS_SERVER|GAA_FLAG_SKIP_MULTICAST|
GAA_FLAG_SKIP_ANYCAST;
DWORD ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapterList, &len);
if (ret == ERROR_BUFFER_OVERFLOW) {
free(adapterList);
adapterList = static_cast<PIP_ADAPTER_ADDRESSES> (moz_xmalloc(len));
ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapterList, &len);
}
if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) {
free(adapterList);
return ERROR_NOT_SUPPORTED;
}
//
// Since NotifyIpInterfaceChange() signals a change more often than we
// think is a worthy change, we checksum the entire state of all interfaces
// that are UP. If the checksum is the same as previous check, nothing
// of interest changed!
//
ULONG sum = 0;
if (ret == ERROR_SUCCESS) {
bool linkUp = false;
for (PIP_ADAPTER_ADDRESSES adapter = adapterList; adapter;
adapter = adapter->Next) {
if (adapter->OperStatus != IfOperStatusUp ||
!adapter->FirstUnicastAddress ||
adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK ||
CheckICSGateway(adapter) ) {
continue;
}
// Add chars from AdapterName to the checksum.
for (int i = 0; adapter->AdapterName[i]; ++i) {
sum <<= 2;
sum += adapter->AdapterName[i];
}
// Add bytes from each socket address to the checksum.
for (PIP_ADAPTER_UNICAST_ADDRESS pip = adapter->FirstUnicastAddress;
pip; pip = pip->Next) {
SOCKET_ADDRESS *sockAddr = &pip->Address;
for (int i = 0; i < sockAddr->iSockaddrLength; ++i) {
sum += (reinterpret_cast<unsigned char *>
(sockAddr->lpSockaddr))[i];
}
}
linkUp = true;
}
mLinkUp = linkUp;
mStatusKnown = true;
}
free(adapterList);
if (mLinkUp) {
/* Store the checksum only if one or more interfaces are up */
mIPInterfaceChecksum = sum;
}
CoUninitialize();
return ret;
}
/**
* Checks the status of all network adapters. If one is up and has a valid IP
* address, sets mLinkUp to true. Sets mStatusKnown to true if the link status
* is definitive.
*/
void
nsNotifyAddrListener::CheckLinkStatus(void)
{
DWORD ret;
const char *event;
bool prevLinkUp = mLinkUp;
ULONG prevCsum = mIPInterfaceChecksum;
LOG(("check status of all network adapters\n"));
// The CheckAdaptersAddresses call is very expensive (~650 milliseconds),
// so we don't want to call it synchronously. Instead, we just start up
// assuming we have a network link, but we'll report that the status is
// unknown.
if (NS_IsMainThread()) {
NS_WARNING("CheckLinkStatus called on main thread! No check "
"performed. Assuming link is up, status is unknown.");
mLinkUp = true;
if (!mStatusKnown) {
event = NS_NETWORK_LINK_DATA_UNKNOWN;
} else if (!prevLinkUp) {
event = NS_NETWORK_LINK_DATA_UP;
} else {
// Known status and it was already UP
event = nullptr;
}
if (event) {
SendEvent(event);
}
} else {
ret = CheckAdaptersAddresses();
if (ret != ERROR_SUCCESS) {
mLinkUp = true;
}
if (mLinkUp && (prevCsum != mIPInterfaceChecksum)) {
TimeDuration since = TimeStamp::Now() - mStartTime;
// Network is online. Topology has changed. Always send CHANGED
// before UP - if allowed to and having cooled down.
if (mAllowChangedEvent && (since.ToMilliseconds() > 2000)) {
NetworkChanged();
}
}
if (prevLinkUp != mLinkUp) {
// UP/DOWN status changed, send appropriate UP/DOWN event
SendEvent(mLinkUp ?
NS_NETWORK_LINK_DATA_UP : NS_NETWORK_LINK_DATA_DOWN);
}
}
}