Skip to content

Commit

Permalink
FXVPN-381: Perform DNS resolution in-process with c-ares (#10304) (#1…
Browse files Browse the repository at this point in the history
…0316)

Many domains resolve their hostnames into addresses which depend on the user's location,
meaning that to truly exclude an application we also need to exclude their DNS queries. In
order to do this correctly, we must make use a nameserver outside the VPN tunnel.

Unfortunately, this is difficult, if not impossible to accomplish with a `QDnsLookup` as it often
uses in-kernel resolvers and has difficulty with the Windows killswitch. Instead we make use
of the c-ares library, which ensures that it can take place in the same process as the socks
proxy.

---------

Co-authored-by: Naomi Kirby <[email protected]>
Co-authored-by: Sebastian Streich <[email protected]>
  • Loading branch information
3 people authored Mar 4, 2025
1 parent 2bc2bbe commit 2515a61
Show file tree
Hide file tree
Showing 21 changed files with 362 additions and 125 deletions.
1 change: 1 addition & 0 deletions .dictionary
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ SHA
Serde
SettingsHolder
Speedtest
Stenberg
SwiftyPing
TBD
TODO
Expand Down
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@
url = https://github.com/mozilla-l10n/mozilla-vpn-client-l10n.git
branch = main
shallow = true
[submodule "3rdparty/c-ares"]
path = 3rdparty/c-ares
url = https://github.com/c-ares/c-ares
28 changes: 28 additions & 0 deletions 3rdparty/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# 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/.

# Nothing we need to do here rn.
if(CMAKE_CROSSCOMPILING)
return()
endif()


# This Policy will make Cmake respect SET
# over the OPTIONS of sub
cmake_policy(SET CMP0077 NEW)

include(FetchContent)

FetchContent_Declare(libcares SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/c-ares")

# Set options before calling FetchContent_MakeAvailable
set(CARES_STATIC ON CACHE BOOL "Build static c-ares" FORCE)
set(CARES_SHARED OFF CACHE BOOL "Disable shared c-ares" FORCE)
set(CARES_BUILD_TESTS OFF CACHE BOOL "Disable c-ares tests" FORCE)
set(CARES_BUILD_CONTAINER_TESTS OFF CACHE BOOL "Disable c-ares container tests" FORCE)
set(CARES_BUILD_TOOLS OFF CACHE BOOL "Disable c-ares tools" FORCE)
set(CARES_INSTALL OFF CACHE BOOL "Disable c-ares global install" FORCE)


FetchContent_MakeAvailable(libcares)
1 change: 1 addition & 0 deletions 3rdparty/c-ares
Submodule c-ares added at b82840
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ if(MSVC)
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang$")
include(src/cmake/clang.cmake)
endif()
# Add External dependencies
add_subdirectory(3rdparty)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
Expand Down
14 changes: 14 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1527,3 +1527,17 @@ DATA FILES OR SOFTWARE.
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written authorization
of the copyright holder.



The MIT License (MIT) - c-ares
=====================================

Copyright (c) 1998 Massachusetts Institute of Technology
Copyright (c) 2007 - 2023 Daniel Stenberg with many contributors, see AUTHORS file.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 changes: 9 additions & 6 deletions extension/socks5proxy/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# No need to compile this for mobile/web
if(NOT CMAKE_CROSSCOMPILING)
add_subdirectory(src)
add_subdirectory(bin)
add_subdirectory(tests)

target_compile_definitions(shared-sources INTERFACE MZ_PROXY_ENABLED)
if(CMAKE_CROSSCOMPILING)
return()
endif()

add_subdirectory(src)
add_subdirectory(bin)
add_subdirectory(tests)

target_compile_definitions(shared-sources INTERFACE MZ_PROXY_ENABLED)



7 changes: 5 additions & 2 deletions extension/socks5proxy/bin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ target_link_libraries(socksproxy PUBLIC
libSocks5proxy
)

if(CMAKE_BUILD_TYPE STREQUAL "Debug")
target_compile_definitions(socksproxy PRIVATE MZ_DEBUG)
endif()

if(WIN32)
target_compile_definitions(socksproxy PRIVATE PROXY_OS_WIN)
target_sources(socksproxy PRIVATE
Expand All @@ -23,8 +27,7 @@ if(WIN32)
winfwpolicy.h
winsvcthread.cpp
winsvcthread.h
winutils.cpp
winutils.h)
)
target_link_libraries(socksproxy PRIVATE Iphlpapi.lib)

install(FILES
Expand Down
2 changes: 2 additions & 0 deletions extension/socks5proxy/bin/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,5 +189,7 @@ int main(int argc, char** argv) {
WinFwPolicy::create(socks5);
#endif

QObject::connect(qApp, &QCoreApplication::aboutToQuit,
[]() { qInfo() << "Shutting down"; });
return app.exec();
}
113 changes: 88 additions & 25 deletions extension/socks5proxy/bin/windowsbypass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <WS2tcpip.h>
#include <fwpmu.h>
#include <iphlpapi.h>
#include <netioapi.h>
#include <windows.h>
#include <winsock2.h>
Expand All @@ -20,10 +21,6 @@
#include "socks5.h"
#include "winutils.h"

// Fixed GUID of the Wireguard NT driver.
constexpr const QUuid WIREGUARD_NT_GUID(0xf64063ab, 0xbfee, 0x4881, 0xbf, 0x79,
0x36, 0x6e, 0x4c, 0xc7, 0xba, 0x75);

// Called by the kernel on network interface changes.
// Runs in some unknown thread, so invoke a Qt signal to do the real work.
static void netChangeCallback(PVOID context, PMIB_IPINTERFACE_ROW row,
Expand Down Expand Up @@ -147,18 +144,6 @@ void WindowsBypass::outgoingConnection(QAbstractSocket* s,
}
}

// static

quint64 WindowsBypass::getVpnLuid() const {
// Get the LUID of the wireguard interface, if it's up.
NET_LUID luid;
GUID vpnInterfaceGuid = WIREGUARD_NT_GUID;
if (ConvertInterfaceGuidToLuid(&vpnInterfaceGuid, &luid) != NO_ERROR) {
return 0;
}
return luid.Value;
}

void WindowsBypass::refreshAddresses() {
// Get the unicast address table.
MIB_UNICASTIPADDRESS_TABLE* table;
Expand All @@ -172,7 +157,7 @@ void WindowsBypass::refreshAddresses() {

// Populate entries.
QHash<quint64, InterfaceData> data;
const quint64 vpnInterfaceLuid = getVpnLuid();
const quint64 vpnInterfaceLuid = WinUtils::getVpnLuid();
for (ULONG i = 0; i < table->NumEntries; i++) {
const MIB_UNICASTIPADDRESS_ROW* row = &table->Table[i];
if (row->SkipAsSource) {
Expand Down Expand Up @@ -215,19 +200,57 @@ void WindowsBypass::refreshAddresses() {
}
}

// Fetch the interface metrics too.
// Fetch the interface metrics.
for (auto i = data.begin(); i != data.end(); i++) {
MIB_IPINTERFACE_ROW row = {0};
row.Family = AF_INET;
row.InterfaceLuid.Value = i.key();
if (GetIpInterfaceEntry(&row) == NO_ERROR) {
i->ipv4metric = row.Metric;
} else {
i->ipv4metric = ULONG_MAX;
}

row.Family = AF_INET6;
row.InterfaceLuid.Value = i.key();
if (GetIpInterfaceEntry(&row) == NO_ERROR) {
i->metric = row.Metric;
i->ipv6metric = row.Metric;
} else {
i->metric = ULONG_MAX;
i->ipv6metric = ULONG_MAX;
}
}

// Fetch the DNS resolvers too.
QByteArray gaaBuffer(4096, 0);
ULONG gaaBufferSize = gaaBuffer.size();
auto adapterAddrs = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(gaaBuffer.data());
result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr,
adapterAddrs, &gaaBufferSize);
if (result == ERROR_BUFFER_OVERFLOW) {
gaaBuffer.resize(gaaBufferSize);
adapterAddrs = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(gaaBuffer.data());
result = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, nullptr,
adapterAddrs, &gaaBufferSize);
}
for (auto i = adapterAddrs; result == NO_ERROR && i != nullptr; i = i->Next) {
// Ignore DNS servers on interfaces without routable addresses.
if (!data.contains(i->Luid.Value)) {
continue;
}
if (i->FirstDnsServerAddress == nullptr) {
continue;
}

// FIXME: Only using the first DNS server for now.
struct sockaddr* sa = i->FirstDnsServerAddress->Address.lpSockaddr;
data[i->Luid.Value].dnsAddr.setAddress(sa);
qDebug() << "Using" << data[i->Luid.Value].dnsAddr.toString()
<< "for DNS server";
}

// Swap the updated table into use.
m_interfaceData.swap(data);
updateNameserver();
}

void WindowsBypass::interfaceChanged(quint64 luid) {
Expand All @@ -239,14 +262,52 @@ void WindowsBypass::interfaceChanged(quint64 luid) {
return;
}

// Update the interface metric.
// Update the interface metrics.
MIB_IPINTERFACE_ROW row = {0};
row.Family = AF_INET;
row.InterfaceLuid.Value = luid;
if (GetIpInterfaceEntry(&row) == NO_ERROR) {
i->metric = row.Metric;
i->ipv4metric = row.Metric;
} else {
i->metric = ULONG_MAX;
i->ipv4metric = ULONG_MAX;
}

row.Family = AF_INET6;
row.InterfaceLuid.Value = luid;
if (GetIpInterfaceEntry(&row) == NO_ERROR) {
i->ipv6metric = row.Metric;
} else {
i->ipv6metric = ULONG_MAX;
}

updateNameserver();
}

void WindowsBypass::updateNameserver() {
// Update the preferred DNS server.
QHostAddress dnsNameserver;
ULONG dnsMetric = ULONG_MAX;
for (auto i = m_interfaceData.constBegin(); i != m_interfaceData.constEnd();
i++) {
auto data = i.value();
if (data.dnsAddr.isNull()) {
continue;
}
if (data.dnsAddr.protocol() == QAbstractSocket::IPv6Protocol) {
if (data.ipv6metric >= dnsMetric) {
continue;
}
dnsMetric = data.ipv6metric;
} else {
if (data.ipv4metric >= dnsMetric) {
continue;
}
dnsMetric = data.ipv4metric;
}
dnsNameserver = data.dnsAddr;
}
qDebug() << "Setting nameserver:" << dnsNameserver.toString();
DNSResolver::instance()->setNameserver(dnsNameserver);
}

// In this function, we basically try our best to re-implement the Windows
Expand Down Expand Up @@ -295,18 +356,20 @@ const MIB_IPFORWARD_ROW2* WindowsBypass::lookupRoute(

// Ensure this route has a valid source address.
auto ifData = m_interfaceData.value(row.InterfaceLuid.Value);
ULONG rowMetric = row.Metric;
if (family == AF_INET) {
if (ifData.ipv4addr.isNull()) {
continue;
}
rowMetric += ifData.ipv4metric;
} else {
if (ifData.ipv6addr.isNull()) {
continue;
}
rowMetric += ifData.ipv6metric;
}

// Choose the route with the best metric in case of a tie.
ULONG rowMetric = row.Metric + ifData.metric;
if (Q_UNLIKELY(rowMetric < row.Metric)) {
rowMetric = ULONG_MAX; // check for saturation arithmetic.
}
Expand Down Expand Up @@ -341,7 +404,7 @@ void WindowsBypass::updateTable(QVector<MIB_IPFORWARD_ROW2>& table,
auto mibGuard = qScopeGuard([mib] { FreeMibTable(mib); });

// First pass: iterate over the table and estimate the size to allocate.
const quint64 vpnInterfaceLuid = getVpnLuid();
const quint64 vpnInterfaceLuid = WinUtils::getVpnLuid();
ULONG tableSize = 0;
for (ULONG i = 0; i < mib->NumEntries; i++) {
if (mib->Table[i].InterfaceLuid.Value != vpnInterfaceLuid) {
Expand Down
5 changes: 4 additions & 1 deletion extension/socks5proxy/bin/windowsbypass.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class WindowsBypass final : public QObject {
private:
quint64 getVpnLuid() const;
void updateTable(QVector<struct _MIB_IPFORWARD_ROW2>& table, int family);
void updateNameserver();
const struct _MIB_IPFORWARD_ROW2* lookupRoute(const QHostAddress& dest) const;

private slots:
Expand All @@ -40,9 +41,11 @@ class WindowsBypass final : public QObject {
void* m_routeChangeHandle = nullptr;

struct InterfaceData {
unsigned long metric;
unsigned long ipv4metric;
unsigned long ipv6metric;
QHostAddress ipv4addr;
QHostAddress ipv6addr;
QHostAddress dnsAddr;
};

QHash<quint64, InterfaceData> m_interfaceData;
Expand Down
3 changes: 3 additions & 0 deletions extension/socks5proxy/bin/winfwpolicy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ void WinFwPolicy::fwpmSublayerChanged(uint changeType,
}

void WinFwPolicy::restrictProxyPort(quint16 port) {
#ifdef MZ_DEBUG
return;
#endif
// Start a transaction so that the firewall changes can be made atomically.
FwpmTransactionBegin0(m_fwEngineHandle, 0);
auto txnGuard =
Expand Down
17 changes: 15 additions & 2 deletions extension/socks5proxy/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,30 @@

qt_add_library(libSocks5proxy STATIC)
set_target_properties(libSocks5proxy PROPERTIES FOLDER "Libs")
target_link_libraries(libSocks5proxy PUBLIC Qt6::Core Qt6::Network)
target_link_libraries(libSocks5proxy PUBLIC
Qt6::Core
Qt6::Network)

add_dependencies(libSocks5proxy c-ares)
target_link_libraries(libSocks5proxy PRIVATE c-ares)

target_compile_definitions(libSocks5proxy PRIVATE CARES_STATICLIB)

target_sources(libSocks5proxy PRIVATE
socks5.h
socks5.cpp
socks5connection.cpp
socks5connection.h
dnsresolver.h
dnsresolver.cpp
)

if(WIN32)
target_sources(libSocks5proxy PRIVATE socks5local_windows.cpp)
target_sources(libSocks5proxy PRIVATE
socks5local_windows.cpp
winutils.cpp
winutils.h
)
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
target_sources(libSocks5proxy PRIVATE socks5local_linux.cpp)
else()
Expand Down
Loading

0 comments on commit 2515a61

Please sign in to comment.