/* 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 "windowsroutemonitor.h" #include #include "leakdetector.h" #include "logger.h" namespace { Logger logger("WindowsRouteMonitor"); }; // namespace // Attempt to mark routing entries that we create with a relatively // high metric. This ensures that we can skip over routes of our own // creation when processing route changes, and ensures that we give // way to other routing entries. constexpr const ULONG EXCLUSION_ROUTE_METRIC = 0x5e72; // Called by the kernel on route changes - perform some basic filtering and // invoke the routeChanged slot to do the real work. static void routeChangeCallback(PVOID context, PMIB_IPFORWARD_ROW2 row, MIB_NOTIFICATION_TYPE type) { WindowsRouteMonitor* monitor = (WindowsRouteMonitor*)context; Q_UNUSED(type); // Ignore route changes that we created. if ((row->Protocol == MIB_IPPROTO_NETMGMT) && (row->Metric == EXCLUSION_ROUTE_METRIC)) { return; } if (monitor->getLuid() == row->InterfaceLuid.Value) { return; } // Invoke the route changed signal to do the real work in Qt. QMetaObject::invokeMethod(monitor, "routeChanged", Qt::QueuedConnection); } // Perform prefix matching comparison on IP addresses in host order. static int prefixcmp(const void* a, const void* b, size_t bits) { size_t bytes = bits / 8; if (bytes > 0) { int diff = memcmp(a, b, bytes); if (diff != 0) { return diff; } } if (bits % 8) { quint8 avalue = *((const quint8*)a + bytes) >> (8 - bits % 8); quint8 bvalue = *((const quint8*)b + bytes) >> (8 - bits % 8); return avalue - bvalue; } return 0; } WindowsRouteMonitor::WindowsRouteMonitor(quint64 luid, QObject* parent) : QObject(parent), m_luid(luid) { MZ_COUNT_CTOR(WindowsRouteMonitor); logger.debug() << "WindowsRouteMonitor created."; NotifyRouteChange2(AF_INET, routeChangeCallback, this, FALSE, &m_routeHandle); } WindowsRouteMonitor::~WindowsRouteMonitor() { MZ_COUNT_DTOR(WindowsRouteMonitor); CancelMibChangeNotify2(m_routeHandle); flushRouteTable(m_exclusionRoutes); flushRouteTable(m_clonedRoutes); logger.debug() << "WindowsRouteMonitor destroyed."; } void WindowsRouteMonitor::updateInterfaceMetrics(int family) { PMIB_IPINTERFACE_TABLE table; DWORD result = GetIpInterfaceTable(family, &table); if (result != NO_ERROR) { logger.warning() << "Failed to retrive interface table." << result; return; } auto guard = qScopeGuard([&] { FreeMibTable(table); }); // Flush the list of interfaces that are valid for routing. if ((family == AF_INET) || (family == AF_UNSPEC)) { m_interfaceMetricsIpv4.clear(); } if ((family == AF_INET6) || (family == AF_UNSPEC)) { m_interfaceMetricsIpv6.clear(); } // Rebuild the list of interfaces that are valid for routing. for (ULONG i = 0; i < table->NumEntries; i++) { MIB_IPINTERFACE_ROW* row = &table->Table[i]; if (row->InterfaceLuid.Value == m_luid) { continue; } if (!row->Connected) { continue; } if (row->Family == AF_INET) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv4 routing"; m_interfaceMetricsIpv4[row->InterfaceLuid.Value] = row->Metric; } if (row->Family == AF_INET6) { logger.debug() << "Interface" << row->InterfaceIndex << "is valid for IPv6 routing"; m_interfaceMetricsIpv6[row->InterfaceLuid.Value] = row->Metric; } } } void WindowsRouteMonitor::updateExclusionRoute(MIB_IPFORWARD_ROW2* data, void* ptable) { PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); SOCKADDR_INET nexthop = {}; quint64 bestLuid = 0; int bestMatch = -1; ULONG bestMetric = ULONG_MAX; nexthop.si_family = data->DestinationPrefix.Prefix.si_family; for (ULONG i = 0; i < table->NumEntries; i++) { MIB_IPFORWARD_ROW2* row = &table->Table[i]; // Ignore routes into the VPN interface. if (row->InterfaceLuid.Value == m_luid) { continue; } if (row->DestinationPrefix.PrefixLength < bestMatch) { continue; } // Ignore routes of our own creation. if ((row->Protocol == data->Protocol) && (row->Metric == data->Metric)) { continue; } // Check if the routing table entry matches the destination. if (!routeContainsDest(&row->DestinationPrefix, &data->DestinationPrefix)) { continue; } // Compute the combined interface and routing metric. ULONG routeMetric = row->Metric; if (data->DestinationPrefix.Prefix.si_family == AF_INET6) { if (!m_interfaceMetricsIpv6.contains(row->InterfaceLuid.Value)) { continue; } routeMetric += m_interfaceMetricsIpv6[row->InterfaceLuid.Value]; } else if (data->DestinationPrefix.Prefix.si_family == AF_INET) { if (!m_interfaceMetricsIpv4.contains(row->InterfaceLuid.Value)) { continue; } routeMetric += m_interfaceMetricsIpv4[row->InterfaceLuid.Value]; } else { // Unsupported destination address family. continue; } if (routeMetric < row->Metric) { routeMetric = ULONG_MAX; } // Prefer routes with lower metric if we find multiple matches // with the same prefix length. if ((row->DestinationPrefix.PrefixLength == bestMatch) && (routeMetric >= bestMetric)) { continue; } // If we got here, then this is the longest prefix match so far. memcpy(&nexthop, &row->NextHop, sizeof(SOCKADDR_INET)); bestMatch = row->DestinationPrefix.PrefixLength; bestMetric = routeMetric; if (bestMatch == data->DestinationPrefix.PrefixLength) { bestLuid = 0; // Don't write to the table if we find an exact match. } else { bestLuid = row->InterfaceLuid.Value; } } // If neither the interface nor next-hop have changed, then do nothing. if (data->InterfaceLuid.Value == bestLuid && memcmp(&nexthop, &data->NextHop, sizeof(SOCKADDR_INET)) == 0) { return; } // Delete the previous routing table entry, if any. if (data->InterfaceLuid.Value != 0) { DWORD result = DeleteIpForwardEntry2(data); if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { logger.error() << "Failed to delete route:" << result; } } // Update the routing table entry. data->InterfaceLuid.Value = bestLuid; memcpy(&data->NextHop, &nexthop, sizeof(SOCKADDR_INET)); if (data->InterfaceLuid.Value != 0) { DWORD result = CreateIpForwardEntry2(data); if (result != NO_ERROR) { logger.error() << "Failed to update route:" << result; } } } // static bool WindowsRouteMonitor::routeContainsDest(const IP_ADDRESS_PREFIX* route, const IP_ADDRESS_PREFIX* dest) { if (route->Prefix.si_family != dest->Prefix.si_family) { return false; } if (route->PrefixLength > dest->PrefixLength) { return false; } if (route->Prefix.si_family == AF_INET) { return prefixcmp(&route->Prefix.Ipv4.sin_addr, &dest->Prefix.Ipv4.sin_addr, route->PrefixLength) == 0; } else if (route->Prefix.si_family == AF_INET6) { return prefixcmp(&route->Prefix.Ipv6.sin6_addr, &dest->Prefix.Ipv6.sin6_addr, route->PrefixLength) == 0; } else { return false; } } // static QHostAddress WindowsRouteMonitor::prefixToAddress( const IP_ADDRESS_PREFIX* dest) { if (dest->Prefix.si_family == AF_INET6) { return QHostAddress(dest->Prefix.Ipv6.sin6_addr.s6_addr); } else if (dest->Prefix.si_family == AF_INET) { quint32 addr = htonl(dest->Prefix.Ipv4.sin_addr.s_addr); return QHostAddress(addr); } else { return QHostAddress(); } } bool WindowsRouteMonitor::isRouteExcluded(const IP_ADDRESS_PREFIX* dest) const { auto i = m_exclusionRoutes.constBegin(); while (i != m_exclusionRoutes.constEnd()) { const MIB_IPFORWARD_ROW2* row = i.value(); if (routeContainsDest(&row->DestinationPrefix, dest)) { return true; } i++; } return false; } void WindowsRouteMonitor::updateCapturedRoutes(int family) { if (!m_defaultRouteCapture) { return; } PMIB_IPFORWARD_TABLE2 table; DWORD error = GetIpForwardTable2(family, &table); if (error != NO_ERROR) { updateCapturedRoutes(family, table); FreeMibTable(table); } } void WindowsRouteMonitor::updateCapturedRoutes(int family, void* ptable) { PMIB_IPFORWARD_TABLE2 table = reinterpret_cast(ptable); if (!m_defaultRouteCapture) { return; } for (ULONG i = 0; i < table->NumEntries; i++) { MIB_IPFORWARD_ROW2* row = &table->Table[i]; // Ignore routes into the VPN interface. if (row->InterfaceLuid.Value == m_luid) { continue; } // Ignore the default route if (row->DestinationPrefix.PrefixLength == 0) { continue; } // Ignore routes of our own creation. if ((row->Protocol == MIB_IPPROTO_NETMGMT) && (row->Metric == EXCLUSION_ROUTE_METRIC)) { continue; } // Ignore routes which should be excluded. if (isRouteExcluded(&row->DestinationPrefix)) { continue; } QHostAddress destination = prefixToAddress(&row->DestinationPrefix); if (destination.isLoopback() || destination.isBroadcast() || destination.isLinkLocal() || destination.isMulticast()) { continue; } // If we get here, this route should be cloned. IPAddress prefix(destination, row->DestinationPrefix.PrefixLength); MIB_IPFORWARD_ROW2* data = m_clonedRoutes.value(prefix, nullptr); if (data != nullptr) { // Count the number of matching entries in the main table. data->Age++; continue; } logger.debug() << "Capturing route to" << logger.sensitive(prefix.toString()); // Clone the route and direct it into the VPN tunnel. data = new MIB_IPFORWARD_ROW2; InitializeIpForwardEntry(data); data->InterfaceLuid.Value = m_luid; data->DestinationPrefix = row->DestinationPrefix; data->NextHop.si_family = data->DestinationPrefix.Prefix.si_family; // Set the rest of the flags for a static route. data->ValidLifetime = 0xffffffff; data->PreferredLifetime = 0xffffffff; data->Metric = 0; data->Protocol = MIB_IPPROTO_NETMGMT; data->Loopback = false; data->AutoconfigureAddress = false; data->Publish = false; data->Immortal = false; data->Age = 0; // Route this traffic into the VPN tunnel. DWORD result = CreateIpForwardEntry2(data); if (result != NO_ERROR) { logger.error() << "Failed to update route:" << result; delete data; } else { m_clonedRoutes.insert(prefix, data); data->Age++; } } // Finally scan for any routes which were removed from the table. We do this // by reusing the age field to count the number of matching entries in the // main table. auto i = m_clonedRoutes.begin(); while (i != m_clonedRoutes.end()) { MIB_IPFORWARD_ROW2* data = i.value(); if (data->Age > 0) { // Entry is in use, don't delete it. data->Age = 0; i++; continue; } if ((family != AF_UNSPEC) && (data->DestinationPrefix.Prefix.si_family != family)) { // We are not processing updates to this address family. i++; continue; } logger.debug() << "Removing route capture for" << logger.sensitive(i.key().toString()); // Otherwise, this route is no longer in use. DWORD result = DeleteIpForwardEntry2(data); if ((result != NO_ERROR) && (result != ERROR_NOT_FOUND)) { logger.error() << "Failed to delete route:" << result; } delete data; i = m_clonedRoutes.erase(i); } } bool WindowsRouteMonitor::addExclusionRoute(const IPAddress& prefix) { logger.debug() << "Adding exclusion route for" << logger.sensitive(prefix.toString()); // Silently ignore non-routeable addresses. QHostAddress addr = prefix.address(); if (addr.isLoopback() || addr.isBroadcast() || addr.isLinkLocal() || addr.isMulticast()) { return true; } if (m_exclusionRoutes.contains(prefix)) { logger.warning() << "Exclusion route already exists"; return false; } // Allocate and initialize the MIB routing table row. MIB_IPFORWARD_ROW2* data = new MIB_IPFORWARD_ROW2; InitializeIpForwardEntry(data); if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { Q_IPV6ADDR buf = prefix.address().toIPv6Address(); memcpy(&data->DestinationPrefix.Prefix.Ipv6.sin6_addr, &buf, sizeof(buf)); data->DestinationPrefix.Prefix.Ipv6.sin6_family = AF_INET6; data->DestinationPrefix.PrefixLength = prefix.prefixLength(); } else { quint32 buf = prefix.address().toIPv4Address(); data->DestinationPrefix.Prefix.Ipv4.sin_addr.s_addr = htonl(buf); data->DestinationPrefix.Prefix.Ipv4.sin_family = AF_INET; data->DestinationPrefix.PrefixLength = prefix.prefixLength(); } data->NextHop.si_family = data->DestinationPrefix.Prefix.si_family; // Set the rest of the flags for a static route. data->ValidLifetime = 0xffffffff; data->PreferredLifetime = 0xffffffff; data->Metric = EXCLUSION_ROUTE_METRIC; data->Protocol = MIB_IPPROTO_NETMGMT; data->Loopback = false; data->AutoconfigureAddress = false; data->Publish = false; data->Immortal = false; data->Age = 0; PMIB_IPFORWARD_TABLE2 table; int family; if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) { family = AF_INET6; } else { family = AF_INET; } DWORD result = GetIpForwardTable2(family, &table); if (result != NO_ERROR) { logger.error() << "Failed to fetch routing table:" << result; delete data; return false; } updateInterfaceMetrics(family); updateCapturedRoutes(family, table); updateExclusionRoute(data, table); FreeMibTable(table); m_exclusionRoutes[prefix] = data; return true; } bool WindowsRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) { logger.debug() << "Deleting exclusion route for" << logger.sensitive(prefix.address().toString()); MIB_IPFORWARD_ROW2* data = m_exclusionRoutes.take(prefix); if (data == nullptr) { return true; } DWORD result = DeleteIpForwardEntry2(data); if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { logger.error() << "Failed to delete route to" << logger.sensitive(prefix.toString()) << "result:" << result; } // Captured routes might have changed. updateCapturedRoutes(data->DestinationPrefix.Prefix.si_family); delete data; return true; } void WindowsRouteMonitor::flushRouteTable( QHash& table) { for (auto i = table.begin(); i != table.end(); i++) { MIB_IPFORWARD_ROW2* data = i.value(); DWORD result = DeleteIpForwardEntry2(data); if ((result != ERROR_NOT_FOUND) && (result != NO_ERROR)) { logger.error() << "Failed to delete route to" << logger.sensitive(i.key().toString()) << "result:" << result; } delete data; } table.clear(); } void WindowsRouteMonitor::setDetaultRouteCapture(bool enable) { m_defaultRouteCapture = enable; // Flush any captured routes when disabling the feature. if (!m_defaultRouteCapture) { flushRouteTable(m_clonedRoutes); return; } } void WindowsRouteMonitor::routeChanged() { logger.debug() << "Routes changed"; PMIB_IPFORWARD_TABLE2 table; DWORD result = GetIpForwardTable2(AF_UNSPEC, &table); if (result != NO_ERROR) { logger.error() << "Failed to fetch routing table:" << result; return; } updateInterfaceMetrics(AF_UNSPEC); updateCapturedRoutes(AF_UNSPEC, table); for (MIB_IPFORWARD_ROW2* data : m_exclusionRoutes) { updateExclusionRoute(data, table); } FreeMibTable(table); }