From 897c0ded060a6c022b32daa938198770e7a4db5c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 16 Apr 2026 09:06:56 -0700 Subject: [PATCH 01/31] Add boringtun as a 3rdparty module --- .gitmodules | 3 +++ 3rdparty/boringtun | 1 + 2 files changed, 4 insertions(+) create mode 160000 3rdparty/boringtun diff --git a/.gitmodules b/.gitmodules index 26a39a4e09..b9dbf3f5fc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [submodule "3rdparty/c-ares"] path = 3rdparty/c-ares url = https://github.com/c-ares/c-ares +[submodule "3rdparty/boringtun"] + path = 3rdparty/boringtun + url = https://github.com/cloudflare/boringtun diff --git a/3rdparty/boringtun b/3rdparty/boringtun new file mode 160000 index 0000000000..fa979202eb --- /dev/null +++ b/3rdparty/boringtun @@ -0,0 +1 @@ +Subproject commit fa979202eb8c41e54fea1ef6069faafc1364d102 From 6fb3691156e914c42b647e9800fa76ba6cd6cae0 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 16 Apr 2026 09:07:45 -0700 Subject: [PATCH 02/31] Add support for specifying features when building rust crates --- scripts/cmake/rustlang.cmake | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/scripts/cmake/rustlang.cmake b/scripts/cmake/rustlang.cmake index 00b3ff320d..e2fa96371a 100644 --- a/scripts/cmake/rustlang.cmake +++ b/scripts/cmake/rustlang.cmake @@ -173,6 +173,7 @@ endfunction() # LIBRARY_FILE: Filename of the expected library to be built. # CARGO_ENV: Environment variables to pass to cargo # SHARED: Whether or not we are building a shared library. Defaults to "false". +# EXTRA_ARGS: Additional arguments to pass to 'cargo build' when building the crate. # # This function generates commands necessary to build static archives # in ${BINARY_DIR}/${ARCH}/debug/ and ${BINARY_DIR}/${ARCH}/release/ @@ -186,7 +187,7 @@ function(build_rust_archives) cmake_parse_arguments(RUST_BUILD "" "ARCH;BINARY_DIR;PACKAGE_DIR;CRATE_NAME" - "CARGO_ENV;SHARED" + "CARGO_ENV;SHARED;EXTRA_ARGS" ${ARGN}) list(APPEND RUST_BUILD_CARGO_ENV CARGO_HOME=${CMAKE_BINARY_DIR}/cargo_home) @@ -229,6 +230,8 @@ function(build_rust_archives) endif() endif() + set(RUST_BUILD_COMMON_ARGS) + list(APPEND RUST_BUILD_COMMON_ARGS --target ${ARCH} --target-dir ${RUST_BUILD_BINARY_DIR} ${RUST_BUILD_EXTRA_ARGS}) if((CMAKE_GENERATOR MATCHES "Ninja") OR (CMAKE_GENERATOR MATCHES "Makefiles") OR XCODE) ## If the generator supports it, we can improve build times by setting # a DEPFILE to let CMake know when the library needs building and when @@ -246,7 +249,7 @@ function(build_rust_archives) JOB_POOL cargo WORKING_DIRECTORY ${RUST_BUILD_PACKAGE_DIR} COMMAND ${CMAKE_COMMAND} -E env ${RUST_BUILD_CARGO_ENV} - ${CARGO_BUILD_TOOL} build --lib --release --target ${ARCH} --target-dir ${RUST_BUILD_BINARY_DIR} + ${CARGO_BUILD_TOOL} build --lib --release ${RUST_BUILD_COMMON_ARGS} ) ## Outputs for the debug build @@ -256,7 +259,7 @@ function(build_rust_archives) JOB_POOL cargo WORKING_DIRECTORY ${RUST_BUILD_PACKAGE_DIR} COMMAND ${CMAKE_COMMAND} -E env ${RUST_BUILD_CARGO_ENV} - ${CARGO_BUILD_TOOL} build --lib --target ${ARCH} --target-dir ${RUST_BUILD_BINARY_DIR} + ${CARGO_BUILD_TOOL} build --lib ${RUST_BUILD_COMMON_ARGS} ) ## Reset our policy changes @@ -274,7 +277,7 @@ function(build_rust_archives) ${RUST_BUILD_BINARY_DIR}/${ARCH}/release/.noexist WORKING_DIRECTORY ${RUST_BUILD_PACKAGE_DIR} COMMAND ${CMAKE_COMMAND} -E env ${RUST_BUILD_CARGO_ENV} - ${CARGO_BUILD_TOOL} build --lib --release --target ${ARCH} --target-dir ${RUST_BUILD_BINARY_DIR} + ${CARGO_BUILD_TOOL} build --lib --release ${RUST_BUILD_COMMON_ARGS} ) ## Outputs for the debug build @@ -284,7 +287,7 @@ function(build_rust_archives) ${RUST_BUILD_BINARY_DIR}/${ARCH}/debug/.noexist WORKING_DIRECTORY ${RUST_BUILD_PACKAGE_DIR} COMMAND ${CMAKE_COMMAND} -E env ${RUST_BUILD_CARGO_ENV} - ${CARGO_BUILD_TOOL} build --lib --target ${ARCH} --target-dir ${RUST_BUILD_BINARY_DIR} + ${CARGO_BUILD_TOOL} build --lib ${${RUST_BUILD_COMMON_ARGS}} ) endif() endfunction() @@ -303,12 +306,13 @@ endfunction() # DEPENDS: Additional files on which the target depends. # SHARED: Whether or not we are building a shared library. Defaults to "false". # FW_NAME: Standalone dylibs need to be wrapped in a framework for distribtuion. Required when building shared lib for iOS. +# FEATURES: Additional features to enable when building the crate. # function(add_rust_library TARGET_NAME) cmake_parse_arguments(RUST_TARGET "" "BINARY_DIR;PACKAGE_DIR;CRATE_NAME" - "ARCH;CARGO_ENV;DEPENDS;SHARED;FW_NAME" + "ARCH;CARGO_ENV;DEPENDS;SHARED;FW_NAME;FEATURES" ${ARGN}) if(NOT RUST_TARGET_SHARED) @@ -363,6 +367,12 @@ function(add_rust_library TARGET_NAME) endif() endif() + set(RUST_TARGET_EXTRA_ARGS) + if(RUST_TARGET_FEATURES) + list(JOIN RUST_TARGET_FEATURES "," RUST_TARGET_FEATURES_JOINED) + list(APPEND RUST_TARGET_EXTRA_ARGS --features ${RUST_TARGET_FEATURES_JOINED}) + endif() + get_rust_library_filename(${RUST_TARGET_SHARED} ${RUST_TARGET_CRATE_NAME}) ## Build the rust library file(s) @@ -374,6 +384,7 @@ function(add_rust_library TARGET_NAME) CRATE_NAME ${RUST_TARGET_CRATE_NAME} CARGO_ENV ${RUST_TARGET_CARGO_ENV} SHARED ${RUST_TARGET_SHARED} + EXTRA_ARGS ${RUST_TARGET_EXTRA_ARGS} ) if(RUST_TARGET_DEPENDS) From e851975207d9f326a82b80b3f2624dcd49811043 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 16 Apr 2026 09:10:22 -0700 Subject: [PATCH 03/31] Add boringtun library to network extension build --- macos/networkextension/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index df36936133..12f9060b20 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -65,6 +65,16 @@ target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/VPNSplitTunnelProvider.mm ) +include(${CMAKE_SOURCE_DIR}/scripts/cmake/rustlang.cmake) +add_rust_library(boringtun + PACKAGE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/boringtun + BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR} + FEATURES ffi-bindings + CRATE_NAME boringtun +) +target_include_directories(networkextension PRIVATE ${CMAKE_SOURCE_DIR}/3rdparty/boringtun/boringtun/src) +target_link_libraries(networkextension PRIVATE boringtun) + # Install an embedded provisioning profile if one exists in the source directory. if((NOT XCODE) AND (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/embedded.provisionprofile)) target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/embedded.provisionprofile) From 2ba9f5be3869c834815fde36cc0834c5d3556440 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 20 Apr 2026 12:03:45 -0700 Subject: [PATCH 04/31] Add class to parse and validate wireguard interface config --- macos/networkextension/interfaceconfig.h | 24 +++++ macos/networkextension/interfaceconfig.mm | 115 ++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 macos/networkextension/interfaceconfig.h create mode 100644 macos/networkextension/interfaceconfig.mm diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h new file mode 100644 index 0000000000..003246f3b4 --- /dev/null +++ b/macos/networkextension/interfaceconfig.h @@ -0,0 +1,24 @@ +/* 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/. */ + +#import +#import + +@interface InterfaceConfig : NSObject + ++ (id)parseFromDict:(NSDictionary *)dict; ++ (id)parseFromCoder:(NSCoder*)coder; + +@property (strong) NSString* privateKey; +@property (strong) nw_endpoint_t deviceIpv4Addr; +@property (strong) nw_endpoint_t deviceIpv6Addr; + +@property (strong) NSString* serverPublicKey; +@property NSUInteger serverPort; +@property (strong) nw_endpoint_t serverIpv4Addr; +@property (strong) nw_endpoint_t serverIpv6Addr; + +@property (strong, readonly) NSDictionary* dict; + +@end diff --git a/macos/networkextension/interfaceconfig.mm b/macos/networkextension/interfaceconfig.mm new file mode 100644 index 0000000000..f3372e0a81 --- /dev/null +++ b/macos/networkextension/interfaceconfig.mm @@ -0,0 +1,115 @@ +/* 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/. */ + +#import "interfaceconfig.h" + +#import + +#include +#include +#include + +@implementation InterfaceConfig + +- (NSString*)findString:(NSString*)key { + NSObject* value = [self.dict objectForKey:key]; + if (value == nil) { + return nil; + } + if (![value isKindOfClass:[NSString class]]) { + return nil; + } + return (NSString*)value; +} + ++ (id)parseFromCoder:(NSCoder*)coder { + return [InterfaceConfig parseFromDict:[[NSDictionary alloc] initWithCoder:coder]]; +} + ++ (id)parseFromDict:(NSDictionary *)dict { + InterfaceConfig* config = [InterfaceConfig new]; + config->_dict = dict; + + config.privateKey = [config findString:@"privateKey"]; + if (!config.privateKey) { + return nil; + } + NSString* deviceIpv4Addr = [config findString:@"deviceIpv4Addr"]; + if (!deviceIpv4Addr) { + return nil; + } else { + struct sockaddr_in sin; + NSString* addr = [deviceIpv4Addr componentsSeparatedByString:@"/"][0]; + + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + if (!inet_pton(AF_INET, addr.UTF8String, &sin.sin_addr.s_addr)) { + return nil; + } + config.deviceIpv4Addr = nw_endpoint_create_address((struct sockaddr*)&sin); + } + NSString* deviceIpv6Addr = [config findString:@"deviceIpv6Addr"]; + if (!deviceIpv6Addr) { + return nil; + } else { + struct sockaddr_in6 sin6; + NSString* addr = [deviceIpv6Addr componentsSeparatedByString:@"/"][0]; + + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + if (!inet_pton(AF_INET6, addr.UTF8String, &sin6.sin6_addr.s6_addr)) { + return nil; + } + config.deviceIpv6Addr = nw_endpoint_create_address((struct sockaddr*)&sin6); + } + + config.serverPublicKey = [config findString:@"serverPublicKey"]; + if (!config.serverPublicKey) { + return nil; + } + + NSObject* serverPort = [config.dict objectForKey:@"serverPort"]; + if ([serverPort isKindOfClass:[NSString class]]) { + config.serverPort = [(NSString*)serverPort intValue]; + } else if ([serverPort isKindOfClass:[NSNumber class]]) { + config.serverPort = [(NSNumber*)serverPort intValue]; + } else { + // default wireguard port. + config.serverPort = 51820; + } + + NSString* serverIpv4Addr = [config findString:@"serverIpv4AddrIn"]; + if (serverIpv4Addr) { + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_port = htons(config.serverPort); + if (!inet_pton(AF_INET, serverIpv4Addr.UTF8String, &sin.sin_addr.s_addr)) { + return nil; + } + + config.serverIpv4Addr = nw_endpoint_create_address((struct sockaddr*)&sin); + } + + NSString* serverIpv6Addr = [dict objectForKey:@"serverIpv6AddrIn"]; + if (serverIpv6Addr) { + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_port = htons(config.serverPort); + if (!inet_pton(AF_INET6, serverIpv6Addr.UTF8String, &sin6.sin6_addr.s6_addr)) { + return nil; + } + + config.serverIpv6Addr = nw_endpoint_create_address((struct sockaddr*)&sin6); + } + + return config; +} + +@end From 5f9b2d8196037ab23557dd76954a02824c164eb9 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 20 Apr 2026 12:45:52 -0700 Subject: [PATCH 05/31] Initial attempt to add tunnel bringup in the network extension --- macos/networkextension/CMakeLists.txt | 4 + .../VPNSplitTunnelProvider.mm | 99 ++-- macos/networkextension/wireguardtunnel.h | 28 + macos/networkextension/wireguardtunnel.mm | 492 ++++++++++++++++++ src/platforms/macos/macoscontroller.h | 2 +- src/platforms/macos/macoscontroller.mm | 9 + 6 files changed, 593 insertions(+), 41 deletions(-) create mode 100644 macos/networkextension/wireguardtunnel.h create mode 100644 macos/networkextension/wireguardtunnel.mm diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index 12f9060b20..10fc76a310 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -59,9 +59,13 @@ target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/bypasstcpflow.mm ${CMAKE_CURRENT_SOURCE_DIR}/bypassudpflow.h ${CMAKE_CURRENT_SOURCE_DIR}/bypassudpflow.mm + ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.mm ${CMAKE_CURRENT_SOURCE_DIR}/main.mm ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.h ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.mm + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.h + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.mm ${CMAKE_CURRENT_SOURCE_DIR}/VPNSplitTunnelProvider.mm ) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index d8e452b994..36daa4d649 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -6,7 +6,9 @@ #import "bypasstcpflow.h" #import "bypassudpflow.h" +#import "interfaceconfig.h" #import "routemanager.h" +#import "wireguardtunnel.h" #include #include @@ -35,6 +37,9 @@ - (void)defaultRouteChanged:(int)family @property (strong) NETransparentProxyNetworkSettings* settings; @property (strong) RouteManager* routeManager; +@property (strong) WireguardTunnel* wireguard; +@property (strong) InterfaceConfig* config; + @property (strong) nw_interface_t ipv4Interface; @property (strong) nw_interface_t ipv6Interface; @property (strong) nw_interface_t vpnInterface; @@ -155,10 +160,20 @@ - (void)startProxyWithOptions:(NSDictionary *)options m_handledUdpFlows = 0; m_handledUnknown = 0; + // Parse the configuration + InterfaceConfig* config = [InterfaceConfig parseFromDict:options]; + if (!config) { + completionHandler([VPNSplitTunnelProvider makeError:1 + withDescription:@"invalid configuration"]); + return; + } + _config = config; + // Start the route manager _routeManager = [RouteManager new]; [self.routeManager startWithDelegate:self]; + self.wireguard = [WireguardTunnel new]; self.settings = [[NETransparentProxyNetworkSettings alloc] initWithTunnelRemoteAddress:self.protocolConfiguration.serverAddress]; // Configure the proxy to capture all traffic @@ -231,65 +246,69 @@ - (void)startProxyWithOptions:(NSDictionary *)options } // Configure the settings. + __unsafe_unretained VPNSplitTunnelProvider* weakSelf = self; [self setTunnelNetworkSettings: self.settings completionHandler:^(NSError* error){ if (error != nil) { NSLog(@"settings error: %@", error.localizedDescription); + completionHandler(error); } else { NSLog(@"settings applied"); + [weakSelf.wireguard startTunnelWithOptions:weakSelf.config + completionHandler:completionHandler]; } - completionHandler(error); }]; } - (void)stopProxyWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler { - NSLog(@"stopping proxy"); + NSLog(@"stopping proxy"); - // Remove captured default routes, if known. - if (self.ipv4Interface) { - NSLog(@"clearing cloned ipv4 route"); + // Remove captured default routes, if known. + if (self.ipv4Interface) { + NSLog(@"clearing cloned ipv4 route"); - struct sockaddr_in sin; - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_len = sizeof(sin); - NSData* dst = [NSData dataWithBytes:&sin length:sizeof(sin)]; + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + NSData* dst = [NSData dataWithBytes:&sin length:sizeof(sin)]; - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dst - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv4Interface) - withGateway:nil - andFlags:RTF_IFSCOPE]; + [self.routeManager rtmSendRoute:RTM_DELETE + toDestination:dst + withPrefix:0 + viaInterface:nw_interface_get_index(self.ipv4Interface) + withGateway:nil + andFlags:RTF_IFSCOPE]; - self.ipv4Interface = nil; - } - if (self.ipv6Interface) { - NSLog(@"clearing cloned ipv6 route"); + self.ipv4Interface = nil; + } + if (self.ipv6Interface) { + NSLog(@"clearing cloned ipv6 route"); - struct sockaddr_in6 sin6; - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(sin6); - NSData* dst = [NSData dataWithBytes:&sin6 length:sizeof(sin6)]; + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + NSData* dst = [NSData dataWithBytes:&sin6 length:sizeof(sin6)]; - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dst - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv6Interface) - withGateway:nil - andFlags:RTF_IFSCOPE]; + [self.routeManager rtmSendRoute:RTM_DELETE + toDestination:dst + withPrefix:0 + viaInterface:nw_interface_get_index(self.ipv6Interface) + withGateway:nil + andFlags:RTF_IFSCOPE]; - self.ipv6Interface = nil; - } - self.routeManager = nil; + self.ipv6Interface = nil; + } + self.routeManager = nil; - NSLog(@"handled tcp flows: %lld", std::atomic_load(&m_handledTcpFlows)); - NSLog(@"handled udp flows: %lld", std::atomic_load(&m_handledUdpFlows)); - NSLog(@"handled unknown flows: %lld", std::atomic_load(&m_handledUnknown)); + NSLog(@"handled tcp flows: %lld", std::atomic_load(&m_handledTcpFlows)); + NSLog(@"handled udp flows: %lld", std::atomic_load(&m_handledUdpFlows)); + NSLog(@"handled unknown flows: %lld", std::atomic_load(&m_handledUnknown)); - completionHandler(); + [self.wireguard stopTunnelWithReason:reason + completionHandler:completionHandler]; } - (BOOL)matchAppFlow:(NEAppProxyFlow*)flow { @@ -406,11 +425,11 @@ - (BOOL)handleNewFlow:(NEAppProxyFlow*) flow { return NO; } -- (void)cancelProxyWithError:(NSError *) error { +- (void)cancelProxyWithError:(NSError *)error { NSLog(@"cancel proxy: %@", error.localizedDescription); } -- (void)handleAppMessage:(NSData *) messageData +- (void)handleAppMessage:(NSData *)messageData completionHandler:(void (^)(NSData*)) completionHandler { NSError* error; NSKeyedUnarchiver* msg = diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h new file mode 100644 index 0000000000..f6ac1414b7 --- /dev/null +++ b/macos/networkextension/wireguardtunnel.h @@ -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/. */ + +#import +#import +#import + +#import "interfaceconfig.h" + +@interface WireguardTunnel : NSObject + +- (void) startTunnelWithOptions:(InterfaceConfig*) options + completionHandler:(void (^)(NSError *error)) completionHandler; + +- (void) stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)()) completionHandler; + +- (void) cancelTunnelWithError:(NSError*)error; + +- (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; + +@property (nonatomic) NSUInteger mtu; + +@property (strong) nw_connection_t connection; +@property (strong) nw_interface_t virtualInterface; + +@end diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm new file mode 100644 index 0000000000..7ae6825fb2 --- /dev/null +++ b/macos/networkextension/wireguardtunnel.mm @@ -0,0 +1,492 @@ +/* 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/. */ + +#import "wireguardtunnel.h" + +#import +#import +#import + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include "wireguard_ffi.h" +}; + +// Private method in the network framework +extern "C" nw_interface_t nw_interface_create_with_index_and_name(int ifindex, const char *ifname); + +@implementation WireguardTunnel { + struct wireguard_tunnel* m_wireguard; + + int m_tunfd; + CFSocketRef m_socket; + CFRunLoopSourceRef m_source; + CFRunLoopTimerRef m_timer; +} + +constexpr const int WG_PACKET_OVERHEAD = 32; +constexpr const int WG_MTU_OVERHEAD = 80; +constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; + +static void wgLog(const char* msg) { + NSLog(@"wg: %s", msg); +} + +static void utunSockCallback(CFSocketRef s, CFSocketCallBackType cbType, + CFDataRef address, const void * data, void *info) { + WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; + NSData* rawData = (__bridge NSData*)data; + if (cbType != kCFSocketDataCallBack) { + NSLog(@"utunSockCallback: unexpected type %d", (int)cbType); + } else if (rawData.length < 4) { + NSLog(@"utunSockCallback: packet truncated"); + } else { + NSData* packet = [rawData subdataWithRange:NSMakeRange(4, rawData.length - 4)]; + [tunnel handleOutbound:packet + withProtocol:htonl(*(uint32_t*)rawData.bytes)]; + } +} + +static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { + WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; + [tunnel handleTimer]; +} + +- (id)init { + self = [super init]; + set_logging_function(wgLog); + + m_tunfd = -1; + m_wireguard = nil; + + _mtu = IPV6_MMTU; + return self; +} + +static NSError* errorFromErrno(int code, NSString* desc) { + if (!desc) { + desc = @"error occurred"; + } + NSString* msg = [NSString stringWithFormat:@"%@: %s", desc, strerror(code)]; + NSLog(@"%@", msg); + + return [NSError errorWithDomain:NSPOSIXErrorDomain + code:code + userInfo:@{NSLocalizedDescriptionKey: msg}]; +} + +- (void) startTunnelWithOptions:(InterfaceConfig *)options + completionHandler:(void (^)(NSError *error)) completionHandler { + m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); + if (m_tunfd < 0) { + [self shutdownTunnel:errorFromErrno(errno, @"tunnel creation failed") + completionHandler:completionHandler]; + return; + } + + // Connect to the utun control kernel service. + struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; + int err = ioctl(m_tunfd, CTLIOCGINFO, &info); + if (err < 0) { + [self shutdownTunnel:errorFromErrno(errno, @"kernel utun lookup failed") + completionHandler:completionHandler]; + return; + } + struct sockaddr_ctl addr = {}; + addr.sc_len = sizeof(addr); + addr.sc_family = AF_SYSTEM; + addr.ss_sysaddr = AF_SYS_CONTROL; + addr.sc_id = info.ctl_id; + addr.sc_unit = 0; + err = connect(m_tunfd, (struct sockaddr*)&addr, sizeof(addr)); + if (err < 0) { + [self shutdownTunnel:errorFromErrno(errno, @"kernel utun connect failed") + completionHandler:completionHandler]; + return; + } + + // Get the tunnel device's name. + struct ifreq ifr; + socklen_t ifnamesize = sizeof(ifr.ifr_name); + err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, + &ifnamesize); + if (err < 0) { + [self shutdownTunnel:errorFromErrno(errno, @"utun name loookup failed") + completionHandler:completionHandler]; + return; + } + int ifindex = if_nametoindex(ifr.ifr_name); + self.virtualInterface = nw_interface_create_with_index_and_name(ifindex, ifr.ifr_name); + + // Assign addresses + if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { + [self shutdownTunnel:err completionHandler:completionHandler]; + return; + } + if (NSError *err = [self setTunnelAddress:options.deviceIpv6Addr]) { + [self shutdownTunnel:err completionHandler:completionHandler]; + return; + } + + // Set a base MTU, it will get updated later. + ifr.ifr_mtu = self.mtu; + if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { + [self shutdownTunnel:errorFromErrno(errno, @"failed to set mtu") + completionHandler:completionHandler]; + return; + } + + + // Bring the device up. + err = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); + if (err != 0) { + [self shutdownTunnel:errorFromErrno(errno, @"failed to get interface flags") + completionHandler:completionHandler]; + return; + } + ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); + err = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); + if (err != 0) { + [self shutdownTunnel:errorFromErrno(errno, @"failed to set device up") + completionHandler:completionHandler]; + return; + } + + // Wrap the tunnel device in a CFSocket + CFSocketContext ctx = { .info = (__bridge void *)self }; + m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, m_tunfd, + kCFSocketDataCallBack, + utunSockCallback, &ctx); + + // Create a source and attach it to the main run loop. + m_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); + + // (Re)-create the Wireguard tunnel structure. + if (m_wireguard) { + tunnel_free(m_wireguard); + } + uint32_t index; + getentropy(&index, sizeof(index)); + + char *addrstr = nw_endpoint_copy_address_string(options.serverIpv4Addr); + NSLog(@"wireguard peer: %s port=%d", addrstr, nw_endpoint_get_port(options.serverIpv4Addr)); + free(addrstr); + + nw_parameters_t params = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); + self.connection = nw_connection_create(options.serverIpv4Addr, params); + nw_connection_set_queue(self.connection, dispatch_get_main_queue()); + + // The tunnel username should contain the base64-encoded server public key and + // the password should hold the device private key. + m_wireguard = new_tunnel(options.privateKey.UTF8String, + options.serverPublicKey.UTF8String, + nil, // Preshared key + 300, // Keepalive period + index % (1U << 24)); + + nw_connection_set_state_changed_handler(self.connection, + ^(nw_connection_state_t state, nw_error_t err) { + // TODO: Implement Me! + if (err) { + CFErrorRef cfError = nw_error_copy_cf_error(err); + NSLog(@"vpn socket error: %@", (__bridge NSError*)cfError); + completionHandler((__bridge NSError*)cfError); + CFRelease(cfError); + } else if (state == nw_connection_state_cancelled || state == nw_connection_state_failed) { + NSLog(@"vpn socket closed"); + [self cancelTunnelWithError:nil]; + } else if (state != nw_connection_state_ready) { + NSLog(@"vpn socket state %d", state); + } else { + NSLog(@"vpn socket opened"); + [self renegotiate]; + [self handleInbound]; + + // Start the timer. + CFRunLoopTimerContext timerContext = { .info = (__bridge void *)self }; + m_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 0.1, 0, 0, + wgTimerCallback, &timerContext); + CFRunLoopAddTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + } + }); + nw_connection_start(self.connection); +} + +- (void) stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)()) completionHandler { + [self shutdownTunnel:nil completionHandler:^(NSError*error){ + completionHandler(); + }]; +} + +- (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { + if (nw_endpoint_get_type(endpoint) != nw_endpoint_type_address) { + return errorFromErrno(EINVAL, @"failed to set tunnel address"); + } + const struct sockaddr *sa = nw_endpoint_get_address(endpoint); + + NSError *err = nil; + int sock = socket(sa->sa_family, SOCK_DGRAM, IPPROTO_IP); + if (sock < 0) { + return errorFromErrno(errno, @"failed to set tunnel address"); + } + + if (sa->sa_family == AF_INET) { + struct ifaliasreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifra_name, nw_interface_get_name(self.virtualInterface), IFNAMSIZ); + memcpy(&ifr.ifra_addr, sa, sa->sa_len); + + // Set the netmask to 255.255.255.255 + struct sockaddr_in *mask = (struct sockaddr_in*)&ifr.ifra_mask; + memset(mask, 0, sizeof(struct sockaddr_in)); + mask->sin_family = AF_INET; + mask->sin_len = sizeof(struct sockaddr_in); + mask->sin_addr.s_addr = 0xffffffff; + + // Do we really need to set a broadcast address? + struct sockaddr_in *bcast = (struct sockaddr_in*)&ifr.ifra_broadaddr; + memset(bcast, 0, sizeof(struct sockaddr_in)); + bcast->sin_family = AF_INET; + bcast->sin_len = sizeof(struct sockaddr_in); + bcast->sin_addr.s_addr = 0xffffffff; + + if (ioctl(sock, SIOCAIFADDR, &ifr) < 0) { + err = errorFromErrno(errno, @"failed to set tunnel address"); + } + } else if (sa->sa_family == AF_INET6) { + struct in6_aliasreq ifr6; + memset(&ifr6, 0, sizeof(ifr6)); + strncpy(ifr6.ifra_name, nw_interface_get_name(self.virtualInterface), IFNAMSIZ); + memcpy(&ifr6.ifra_addr, sa, sa->sa_len); + ifr6.ifra_lifetime.ia6t_vltime = 0xffffffff; + ifr6.ifra_lifetime.ia6t_pltime = 0xffffffff; + + // Set the prefix length to 128 + struct sockaddr_in6 *mask = (struct sockaddr_in6*)&ifr6.ifra_prefixmask; + mask->sin6_family = AF_INET6; + mask->sin6_len = sizeof(struct sockaddr_in6); + memset(&mask->sin6_addr, 0xff, sizeof(struct in6_addr)); + + if (ioctl(sock, SIOCAIFADDR_IN6, &ifr6) < 0) { + err = errorFromErrno(errno, @"failed to set tunnel address"); + } + } else { + // We don't recognize this address type. + err = errorFromErrno(EAFNOSUPPORT, @"failed to set tunnel address"); + } + close(sock); + return err; +} + +- (void) renegotiate { + NSLog(@"wireguard renegotiate"); + UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); + NSMutableData* buffer = [NSMutableData dataWithLength:WG_MAX_HANDSHAKE_SIZE]; + struct wireguard_result r; + r = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + + dispatch_data_t data = dispatch_data_create(handshake, r.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_FREE); + [self handleWireguard:(int)r.op withData:data]; +} + +- (void) handleTimer { + UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); + struct wireguard_result r; + r = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + + dispatch_data_t data = dispatch_data_create(handshake, r.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_FREE); + [self handleWireguard:(int)r.op withData:data]; +} + +- (void) handleOutbound:(NSData*)packet + withProtocol:(int)protocol { + if (!m_wireguard) { + return; + } + + // I think there is a small bug in boringtun to do with message padding. + // The wireguard protocol states that the encapsulated packet must first + // be padded out to a multiple of 16 bytes in length, but boringtun does + // no such padding during encryption. So let's do it manually ourself. + NSMutableData* plaintext = [NSMutableData dataWithData:packet]; + int tail = plaintext.length % 16; + if (tail) { + [plaintext increaseLengthBy:16 - tail]; + } + + + // Encrypt the packet. + size_t length = plaintext.length + WG_PACKET_OVERHEAD; + uint8_t* ciphertext = (uint8_t*)malloc(length); + struct wireguard_result r; + r = wireguard_write(m_wireguard, (const uint8_t*)plaintext.bytes, + plaintext.length, ciphertext, length); + + dispatch_data_t data = dispatch_data_create(ciphertext, r.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_FREE); + [self handleWireguard:(int)r.op withData:data]; +} + +- (void) handleInbound { + nw_connection_receive(self.connection, 1, UINT16_MAX, + ^(dispatch_data_t data, nw_content_context_t ctx, bool done, nw_error_t err){ + if (err) { + CFErrorRef cfError = nw_error_copy_cf_error(err); + NSLog(@"recv error: %@", (__bridge NSError *)cfError); + CFRelease(cfError); + return; + } + if (!data) { + //[self closeConnection:nil completionHandler:completionHandler]; + return; + } + + size_t length; + const void *ciphertext; + dispatch_data_t __unused map = dispatch_data_create_map(data, &ciphertext, &length); + uint8_t* plaintext = (uint8_t*)malloc(length); + NSLog(@"wireguard recv: %zu", length); + + // Decrypt the wireguard packet. + struct wireguard_result r; + r = wireguard_read(m_wireguard, (const uint8_t*)ciphertext, length, + plaintext, length); + + dispatch_data_t packet = dispatch_data_create(plaintext, r.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_FREE); + [self handleWireguard:(int)r.op withData:packet]; + }); +} + +- (void) handleWireguard:(int)op + withData:(dispatch_data_t)data { + switch (op) { + case WIREGUARD_DONE: + break; + + case WRITE_TO_NETWORK: + NSLog(@"wireguard send: %zu", dispatch_data_get_size(data)); + nw_connection_send(self.connection, data, + NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, + ^(nw_error_t error) { + if (error) { + CFErrorRef cfError = nw_error_copy_cf_error(error); + NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); + CFRelease(cfError); + } else { + NSLog(@"wireguard send okay"); + } + }); + break; + + case WIREGUARD_ERROR: + NSLog(@"wireguard error"); + break; + + case WRITE_TO_TUNNEL_IPV4: + [[fallthrough]]; + case WRITE_TO_TUNNEL_IPV6: + NSLog(@"wireguard send: %zu", dispatch_data_get_size(data)); + NSData* packet = (NSData*)data; + uint32_t header = (op == WRITE_TO_TUNNEL_IPV6) ? htonl(AF_INET6) : htonl(AF_INET); + const struct iovec iov[2] = { + {.iov_base = &header, .iov_len = sizeof(header)}, + {.iov_base = (void *)packet.bytes, .iov_len = packet.length}, + }; + int err = writev(m_tunfd, iov, 2); + NSLog(@"utun write: %d", err); + break; + } +} + +- (void)shutdownTunnel:(NSError*)error + completionHandler:(void (^)(NSError *error)) completionHandler { + if (error) { + NSLog(@"wireguard shutdown: %@", error); + } else { + NSLog(@"wireguard shutdown"); + } + self.connection = nil; + self.virtualInterface = nil; + + if (m_timer) { + CFRunLoopRemoveTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + CFRelease(m_timer); + m_timer = nil; + } + + if (m_source) { + CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); + CFRelease(m_source); + m_source = nil; + } + + if (m_socket) { + CFSocketInvalidate(m_socket); + CFRelease(m_socket); + } + + if (m_tunfd >= 0) { + close(m_tunfd); + m_tunfd = -1; + } + + if (m_wireguard) { + tunnel_free(m_wireguard); + m_wireguard = nil; + } + + if (completionHandler) { + completionHandler(error); + } +} + +- (void)cancelTunnelWithError:(NSError*)error { + [self shutdownTunnel:error completionHandler:nil]; +} + +- (void)setMtu:(NSUInteger)mtu { + _mtu = mtu; + if (m_tunfd < 0) { + return; + } + + struct ifreq ifr; + socklen_t ifnamesize = sizeof(ifr.ifr_name); + int err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, + &ifnamesize); + if (err < 0) { + NSLog(@"mtu failed to find ifname: %s", strerror(errno)); + return; + } + + ifr.ifr_mtu = _mtu; + if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { + NSLog(@"mtu update failed: %s", strerror(errno)); + } +} + +@end diff --git a/src/platforms/macos/macoscontroller.h b/src/platforms/macos/macoscontroller.h index 0fa5d0a930..d51c3578e9 100644 --- a/src/platforms/macos/macoscontroller.h +++ b/src/platforms/macos/macoscontroller.h @@ -32,7 +32,7 @@ class MacOSController final : public ControllerImpl { void forceDaemonCrash() override; - bool multihopSupported() override { return true; } + bool multihopSupported() override; bool splitTunnelSupported() const override; diff --git a/src/platforms/macos/macoscontroller.mm b/src/platforms/macos/macoscontroller.mm index c1249e6a85..ffb2074eb8 100644 --- a/src/platforms/macos/macoscontroller.mm +++ b/src/platforms/macos/macoscontroller.mm @@ -257,6 +257,9 @@ emit initialized(true, jsObj.value("connected").toBool(), // Serialize the interface configuration. NSMutableDictionary* options = [NSMutableDictionary dictionary]; + [options setObject:config.m_privateKey.toNSString() forKey:@"privateKey"]; + [options setObject:config.m_deviceIpv4Address.toNSString() forKey:@"deviceIpv4Addr"]; + [options setObject:config.m_deviceIpv6Address.toNSString() forKey:@"deviceIpv6Addr"]; [options setObject:config.m_serverPublicKey.toNSString() forKey:@"serverPublicKey"]; [options setObject:config.m_serverIpv4AddrIn.toNSString() forKey:@"serverIpv4AddrIn"]; [options setObject:config.m_serverIpv6AddrIn.toNSString() forKey:@"serverIpv6AddrIn"]; @@ -342,6 +345,12 @@ emit initialized(true, jsObj.value("connected").toBool(), [remoteObject() cleanupBackendLogs]; } +bool MacOSController::multihopSupported() { + // The daemon always supports multihop. + // The network extension only supports single-hop for now. + return !splitTunnelSupported(); +} + bool MacOSController::splitTunnelSupported() const { auto loader = static_cast(m_loader); return (loader.manager != nil) && loader.manager.enabled; From f2d3de30de387fba5c2b38ac86c1a22a38f7cce2 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 21 Apr 2026 15:50:10 -0700 Subject: [PATCH 06/31] Split MacOS controller implementations for network extension --- macos/networkextension/wireguardtunnel.h | 9 + macos/networkextension/wireguardtunnel.mm | 41 ++++ src/cmake/macos.cmake | 4 +- src/controller.cpp | 5 + src/platforms/macos/macoscontroller.h | 11 +- src/platforms/macos/macoscontroller.mm | 127 ---------- .../macos/macosextensioncontroller.h | 47 ++++ .../macos/macosextensioncontroller.mm | 229 ++++++++++++++++++ src/platforms/macos/macosextensionloader.h | 17 -- src/platforms/macos/macosextensionloader.mm | 110 --------- 10 files changed, 334 insertions(+), 266 deletions(-) create mode 100644 src/platforms/macos/macosextensioncontroller.h create mode 100644 src/platforms/macos/macosextensioncontroller.mm delete mode 100644 src/platforms/macos/macosextensionloader.h delete mode 100644 src/platforms/macos/macosextensionloader.mm diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h index f6ac1414b7..e32a267097 100644 --- a/macos/networkextension/wireguardtunnel.h +++ b/macos/networkextension/wireguardtunnel.h @@ -8,6 +8,14 @@ #import "interfaceconfig.h" +@interface WireguardStats : NSObject +@property (strong) NSDate *lastHandshake; +@property NSUInteger txBytes; +@property NSUInteger rxBytes; +@property float estimatedLoss; +@property NSUInteger estimatedRtt; +@end + @interface WireguardTunnel : NSObject - (void) startTunnelWithOptions:(InterfaceConfig*) options @@ -21,6 +29,7 @@ - (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; @property (nonatomic) NSUInteger mtu; +@property (strong, readonly, getter=getStatus) WireguardStats* stats; @property (strong) nw_connection_t connection; @property (strong) nw_interface_t virtualInterface; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 7ae6825fb2..5d697314a9 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -21,6 +21,7 @@ #include #include #include +#include #include extern "C" { @@ -30,6 +31,10 @@ // Private method in the network framework extern "C" nw_interface_t nw_interface_create_with_index_and_name(int ifindex, const char *ifname); +@implementation WireguardStats +// Nothing to see here. +@end + @implementation WireguardTunnel { struct wireguard_tunnel* m_wireguard; @@ -37,6 +42,8 @@ @implementation WireguardTunnel { CFSocketRef m_socket; CFRunLoopSourceRef m_source; CFRunLoopTimerRef m_timer; + + struct timespec m_lastHandshake; } constexpr const int WG_PACKET_OVERHEAD = 32; @@ -374,6 +381,17 @@ - (void) handleInbound { r = wireguard_read(m_wireguard, (const uint8_t*)ciphertext, length, plaintext, length); + // After processing a handshake response, update the lastHandshake time + // if it looks and smells like the handshake was successful. + if (*(const uint8_t*)ciphertext == 0x02) { + struct stats wgStats = wireguard_stats(m_wireguard); + if (wgStats.time_since_last_handshake < 0) { + memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); + } else if (wgStats.time_since_last_handshake < 5) { + clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); + } + } + dispatch_data_t packet = dispatch_data_create(plaintext, r.size, dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_FREE); @@ -468,6 +486,29 @@ - (void)cancelTunnelWithError:(NSError*)error { [self shutdownTunnel:error completionHandler:nil]; } +- (WireguardStats*)getStatus { + WireguardStats* result = [WireguardStats new]; + + // Get the handshake time from the timestamp of the last received handshake + // response packet, this will yeild better precision than the stats we get + // from the boringtun API. + if (m_lastHandshake.tv_sec || m_lastHandshake.tv_nsec) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + NSTimeInterval diff = (m_lastHandshake.tv_sec - now.tv_sec); + diff += (m_lastHandshake.tv_nsec - now.tv_nsec) / (1000000000.0); + result.lastHandshake = [NSDate dateWithTimeIntervalSinceNow:diff]; + } + + // Fetch the rest of the stats directly from boringtun. + struct stats wgStats = wireguard_stats(m_wireguard); + result.txBytes = wgStats.tx_bytes; + result.rxBytes = wgStats.rx_bytes; + result.estimatedLoss = wgStats.estimated_loss; + result.estimatedRtt = wgStats.estimated_rtt; + return result; +} + - (void)setMtu:(NSUInteger)mtu { _mtu = mtu; if (m_tunfd < 0) { diff --git a/src/cmake/macos.cmake b/src/cmake/macos.cmake index c6a03c1add..ecd7404ac4 100644 --- a/src/cmake/macos.cmake +++ b/src/cmake/macos.cmake @@ -50,6 +50,8 @@ target_sources(mozillavpn PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macoscontroller.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macoscryptosettings.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macoscryptosettings.mm + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosextensioncontroller.h + ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosextensioncontroller.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosmenubar.cpp ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosmenubar.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macospingsender.cpp @@ -60,8 +62,6 @@ target_sources(mozillavpn PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macossystemtraynotificationhandler.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosnetworkwatcher.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosnetworkwatcher.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosextensionloader.h - ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosextensionloader.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosstatusicon.mm ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosstatusicon.h ${CMAKE_CURRENT_SOURCE_DIR}/platforms/macos/macosutils.mm diff --git a/src/controller.cpp b/src/controller.cpp index 724394d462..2dd6732441 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -40,6 +40,7 @@ # include "platforms/linux/linuxcontroller.h" #elif defined(MZ_MACOS) # include "platforms/macos/macoscontroller.h" +# include "platforms/macos/macosextensioncontroller.h" #elif defined(MZ_IOS) # include "platforms/ios/ioscontroller.h" #elif defined(MZ_ANDROID) @@ -138,7 +139,11 @@ void Controller::initialize() { #elif defined(MZ_LINUX) m_impl.reset(new LinuxController()); #elif defined(MZ_MACOS) + if (Feature::isEnabled(Feature::networkExtension)) { + m_impl.reset(new MacOSExtensionController()); + } else { m_impl.reset(new MacOSController()); + } #elif defined(MZ_IOS) m_impl.reset(new IOSController()); #elif defined(MZ_ANDROID) diff --git a/src/platforms/macos/macoscontroller.h b/src/platforms/macos/macoscontroller.h index d51c3578e9..4ad747fc0b 100644 --- a/src/platforms/macos/macoscontroller.h +++ b/src/platforms/macos/macoscontroller.h @@ -32,9 +32,7 @@ class MacOSController final : public ControllerImpl { void forceDaemonCrash() override; - bool multihopSupported() override; - - bool splitTunnelSupported() const override; + bool multihopSupported() override { return true; } private slots: void upgradeService(); @@ -45,9 +43,6 @@ class MacOSController final : public ControllerImpl { NSString* plist() const; NSString* machServiceName() const; - bool sendSplitTunnelMessage(const QString& actions, - const QStringList& apps = QStringList()) const; - private: QString plistName() const; #ifdef __OBJC__ @@ -60,10 +55,6 @@ class MacOSController final : public ControllerImpl { // NSXPCConnection to the daemon. void* m_connection = nullptr; - - // Split tunneling driver. - void* m_loader = nullptr; - void* m_session = nullptr; }; #endif // MACOSCONTROLLER_H diff --git a/src/platforms/macos/macoscontroller.mm b/src/platforms/macos/macoscontroller.mm index ffb2074eb8..7223787371 100644 --- a/src/platforms/macos/macoscontroller.mm +++ b/src/platforms/macos/macoscontroller.mm @@ -12,9 +12,7 @@ #include #include "constants.h" -#include "feature/features.h" #include "logger.h" -#include "macosextensionloader.h" #include "macosutils.h" #include "version.h" #include "xpcdaemonprotocol.h" @@ -46,26 +44,6 @@ - (id)initWithObject:(ControllerImpl*)controller; m_connectTimer.setSingleShot(true); connect(&m_connectTimer, &QTimer::timeout, this, &MacOSController::connectService); - - // Load the system extension if the networkExtension feature is enabled. - if (Feature::isEnabled(Feature::networkExtension)) { - // Create the Obj-C loader class. - MacosExtensionLoader* loader = [MacosExtensionLoader new]; - [loader retain]; - m_loader = loader; - - // Create a request to install the system extension. - dispatch_queue_t queue = - dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - OSSystemExtensionRequest* req = - [OSSystemExtensionRequest activationRequestForExtension: loader.identifier - queue: queue]; - req.delegate = loader; - - // Start the request - logger.debug() << "activation request started:" << req.identifier; - [[OSSystemExtensionManager sharedManager] submitRequest: req]; - } } MacOSController::~MacOSController() { @@ -74,10 +52,6 @@ - (id)initWithObject:(ControllerImpl*)controller; [conn invalidate]; [conn release]; } - - if (m_loader) { - [static_cast(m_loader) release]; - } } NSString* MacOSController::plist() const { @@ -247,63 +221,10 @@ emit initialized(true, jsObj.value("connected").toBool(), Controller::Reason reason) { QString json = QString::fromUtf8(QJsonDocument(config.toJson()).toJson()); [remoteObject() activate:json.toNSString()]; - - // Create a new tunnel provider session. - auto loader = static_cast(m_loader); - if (!loader || (loader.manager == nil) || !loader.manager.enabled) { - // Split tunnelling is not supported. - return; - } - - // Serialize the interface configuration. - NSMutableDictionary* options = [NSMutableDictionary dictionary]; - [options setObject:config.m_privateKey.toNSString() forKey:@"privateKey"]; - [options setObject:config.m_deviceIpv4Address.toNSString() forKey:@"deviceIpv4Addr"]; - [options setObject:config.m_deviceIpv6Address.toNSString() forKey:@"deviceIpv6Addr"]; - [options setObject:config.m_serverPublicKey.toNSString() forKey:@"serverPublicKey"]; - [options setObject:config.m_serverIpv4AddrIn.toNSString() forKey:@"serverIpv4AddrIn"]; - [options setObject:config.m_serverIpv6AddrIn.toNSString() forKey:@"serverIpv6AddrIn"]; - [options setObject:config.m_serverIpv4Gateway.toNSString() forKey:@"serverIpv4Gateway"]; - [options setObject:config.m_serverIpv6Gateway.toNSString() forKey:@"serverIpv6Gateway"]; - [options setObject:[NSNumber numberWithInt:config.m_serverPort] forKey:@"serverPort"]; - - // Serialize the excluded application list. - NSMutableArray* vpnDisabledApps = - [NSMutableArray arrayWithCapacity:config.m_vpnDisabledApps.length()]; - for (const QString& appId : config.m_vpnDisabledApps) { - [vpnDisabledApps addObject:appId.toNSString()]; - } - [options setObject:vpnDisabledApps forKey:@"apps"]; - - // Get a session and start it. - NSError* error = nil; - NETunnelProviderSession* session = - static_cast(loader.manager.connection); - - // Start the split tunnel proxy. - BOOL okay = [session startTunnelWithOptions:options andReturnError:&error]; - if (error) { - logger.warning() << "proxy start error:" << error.localizedDescription; - } else if (!okay) { - logger.warning() << "proxy start failed"; - } else { - // Save the session and retain it. - [session retain]; - m_session = session; - } } void MacOSController::deactivate() { [remoteObject() deactivate]; - - if (m_session) { - NETunnelProviderSession* session = - static_cast(m_session); - // Stop the split tunnel proxy. - [session stopTunnel]; - [session release]; - m_session = nullptr; - } } void MacOSController::checkStatus() { @@ -345,54 +266,6 @@ emit initialized(true, jsObj.value("connected").toBool(), [remoteObject() cleanupBackendLogs]; } -bool MacOSController::multihopSupported() { - // The daemon always supports multihop. - // The network extension only supports single-hop for now. - return !splitTunnelSupported(); -} - -bool MacOSController::splitTunnelSupported() const { - auto loader = static_cast(m_loader); - return (loader.manager != nil) && loader.manager.enabled; -} - -bool MacOSController::sendSplitTunnelMessage(const QString& action, - const QStringList& apps) const { - if (!m_session) { - logger.debug() << "Split tunneling" << action << "failed: not running"; - return false; - } - - NSKeyedArchiver* encoder = - [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; - [encoder encodeObject:action.toNSString() - forKey:@"action"]; - - if (!apps.isEmpty()) { - NSMutableArray* array = [[NSMutableArray new] init]; - for (const QString& appId : apps) { - [array addObject:appId.toNSString()]; - } - [encoder encodeObject:array - forKey:@"apps"]; - } - - [encoder finishEncoding]; - - NSError* error = nil; - NETunnelProviderSession* session = - static_cast(m_session); - [session sendProviderMessage:encoder.encodedData - returnError:&error - responseHandler:nil]; - - if (error != nil) { - logger.debug() << "Split tunneling" << action << "failed:" << error; - return false; - } - return true; -} - void MacOSController::forceDaemonCrash() { if (m_connection == nullptr) { logger.error() << "Daemon does not seem to be running"; diff --git a/src/platforms/macos/macosextensioncontroller.h b/src/platforms/macos/macosextensioncontroller.h new file mode 100644 index 0000000000..4758738ecf --- /dev/null +++ b/src/platforms/macos/macosextensioncontroller.h @@ -0,0 +1,47 @@ +/* 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/. */ + +#ifndef MACOSEXTENSIONCONTROLLER_H +#define MACOSEXTENSIONCONTROLLER_H + +#include "controllerimpl.h" + +#include + +Q_FORWARD_DECLARE_OBJC_CLASS(NETransparentProxyManager); +Q_FORWARD_DECLARE_OBJC_CLASS(NETunnelProviderSession); + +class MacOSExtensionController final : public ControllerImpl { + Q_OBJECT + Q_DISABLE_COPY_MOVE(MacOSExtensionController) + + public: + MacOSExtensionController(); + ~MacOSExtensionController(); + + void initialize(const Device* device, const Keys* keys) override; + + void activate(const InterfaceConfig& config, Controller::Reason reason) override; + + void deactivate() override; + + void checkStatus() override; + + bool splitTunnelSupported() const override { return true; } + + private slots: + void extLoaderSuccess(int result); + void extLoaderFailure(const QString& reason); + void extNeedsApproval(); + + private: + static NSString* extIdentifier(); + + private: + void* m_delegate = nullptr; + NETransparentProxyManager* m_manager = nullptr; + NETunnelProviderSession* m_session = nullptr; +}; + +#endif // MACOSEXTENSIONCONTROLLER_H diff --git a/src/platforms/macos/macosextensioncontroller.mm b/src/platforms/macos/macosextensioncontroller.mm new file mode 100644 index 0000000000..fb65264a3a --- /dev/null +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -0,0 +1,229 @@ +/* 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 "macosextensioncontroller.h" + +#import +#import +#import + +#include + +#include "logger.h" +#include "macosutils.h" + +// An extension loader - used to forward Obj-C messages back to Qt. +@interface MacOSExtensionDelegate : NSObject +@property MacOSExtensionController* parent; +- (id)initWithObject:(MacOSExtensionController*)controller; +@end + +namespace { +Logger logger("MacOSExtensionController"); +} // namespace + +MacOSExtensionController::MacOSExtensionController() : ControllerImpl() { + // Create the system extension loader delegate. + m_delegate = [[MacOSExtensionDelegate alloc] initWithObject:this]; + [static_cast(m_delegate) retain]; +} + +MacOSExtensionController::~MacOSExtensionController() { + [static_cast(m_delegate) release]; +} + +void MacOSExtensionController::initialize(const Device* device, const Keys* keys) { + // Create a request to install the system extension. + dispatch_queue_t queue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + OSSystemExtensionRequest* req = + [OSSystemExtensionRequest activationRequestForExtension: extIdentifier() + queue: queue]; + req.delegate = static_cast(m_delegate); + + // Start the request + logger.debug() << "activation request started:" << req.identifier; + [[OSSystemExtensionManager sharedManager] submitRequest: req]; +} + +NSString* MacOSExtensionController::extIdentifier() { + return MacOSUtils::appId(".network-extension").toNSString(); +} + +void MacOSExtensionController::extLoaderSuccess(int result) { + logger.info() << "activation request complete:" << result; + + // Start by loading all the proxy managers. + [NETransparentProxyManager loadAllFromPreferencesWithCompletionHandler:^(NSArray* managers, NSError* err){ + if (err != nil) { + logger.debug() << "activation setup failed:" << err; + emit initialized(false, false, QDateTime()); + return; + } + + // Check if an existing manager can be used. + NSString* extId = MacOSExtensionController::extIdentifier(); + for (NETransparentProxyManager* mgr in managers) { + if (![mgr.protocolConfiguration isKindOfClass:[NETunnelProviderProtocol class]]) { + continue; + } + NETunnelProviderProtocol* proto = + static_cast(mgr.protocolConfiguration); + if ([proto.providerBundleIdentifier isEqualToString:extId]) { + logger.info() << "proxy manager found for:" << proto.providerBundleIdentifier; + m_manager = mgr; + break; + } + } + + // Otherwise - create a new manager. + if (m_manager == nil) { + m_manager = [NETransparentProxyManager new]; + m_manager.localizedDescription = @"Mozilla VPN Network Extension"; + logger.info() << "proxy manager created for:" << extId; + } + + // Update the tunnel configuration. + auto protocol = [NETunnelProviderProtocol new]; + protocol.providerBundleIdentifier = extId; + protocol.serverAddress = @"127.0.0.1"; + m_manager.protocolConfiguration = protocol; + + // Enable the manager and sync preferences + m_manager.enabled = true; + [m_manager saveToPreferencesWithCompletionHandler:^(NSError* saveErr){ + if (saveErr != nil) { + logger.debug() << "proxy prefs setup failed:" + << saveErr.localizedDescription; + } + [m_manager loadFromPreferencesWithCompletionHandler:^(NSError* loadErr){ + if (loadErr != nil) { + logger.debug() << "proxy prefs load failed:" + << loadErr.localizedDescription; + } + emit initialized(true, false, QDateTime()); + }]; + }]; + }]; +} + +void MacOSExtensionController::extLoaderFailure(const QString& reason) { + logger.warning() << "activation request failed:" << reason; +} + +void MacOSExtensionController::extNeedsApproval() { + logger.warning() << "activation request needs user approval"; + emit permissionRequired(); +} + +void MacOSExtensionController::activate(const InterfaceConfig& config, + Controller::Reason reason) { + Q_UNUSED(reason); + + // Create a new tunnel provider session. + if ((m_manager == nil) || !m_manager.enabled) { + // Split tunnelling is not supported. + return; + } + + // Serialize the interface configuration. + NSMutableDictionary* options = [NSMutableDictionary dictionary]; + [options setObject:config.m_privateKey.toNSString() forKey:@"privateKey"]; + [options setObject:config.m_deviceIpv4Address.toNSString() forKey:@"deviceIpv4Addr"]; + [options setObject:config.m_deviceIpv6Address.toNSString() forKey:@"deviceIpv6Addr"]; + [options setObject:config.m_serverPublicKey.toNSString() forKey:@"serverPublicKey"]; + [options setObject:config.m_serverIpv4AddrIn.toNSString() forKey:@"serverIpv4AddrIn"]; + [options setObject:config.m_serverIpv6AddrIn.toNSString() forKey:@"serverIpv6AddrIn"]; + [options setObject:config.m_serverIpv4Gateway.toNSString() forKey:@"serverIpv4Gateway"]; + [options setObject:config.m_serverIpv6Gateway.toNSString() forKey:@"serverIpv6Gateway"]; + [options setObject:[NSNumber numberWithInt:config.m_serverPort] forKey:@"serverPort"]; + + // Serialize the excluded application list. + NSMutableArray* vpnDisabledApps = + [NSMutableArray arrayWithCapacity:config.m_vpnDisabledApps.length()]; + for (const QString& appId : config.m_vpnDisabledApps) { + [vpnDisabledApps addObject:appId.toNSString()]; + } + [options setObject:vpnDisabledApps forKey:@"apps"]; + + // Start the split tunnel proxy. + NSError* error = nil; + NETunnelProviderSession* session = + static_cast(m_manager.connection); + BOOL okay = [session startTunnelWithOptions:options andReturnError:&error]; + if (error) { + logger.warning() << "proxy start error:" << error.localizedDescription; + } else if (!okay) { + logger.warning() << "proxy start failed"; + } else { + // Save the session and retain it. + m_session = [session retain]; + emit connected(config.m_serverPublicKey); + } +} + +void MacOSExtensionController::deactivate() { + if (m_session) { + // Stop the split tunnel proxy. + [m_session stopTunnel]; + [m_session release]; + m_session = nullptr; + } +} + +void MacOSExtensionController::checkStatus() { + if (!m_session) { + // Extension is not running. + return; + } + + NSKeyedArchiver* msg = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; + [msg encodeObject:@"status" forKey:@"action"]; + [msg finishEncoding]; + + NSError* error = nil; + [m_session sendProviderMessage:msg.encodedData + returnError:&error + responseHandler:^(NSData* response){ + // TODO: Parse the status and emit something. + }]; + + if (error != nil) { + logger.debug() << "Split tunneling status failed:" << error; + return; + } +} + +@implementation MacOSExtensionDelegate +- (id)initWithObject:(MacOSExtensionController*)controller { + self = [super init]; + self.parent = controller; + return self; +} + +- (void) request:(OSSystemExtensionRequest *) request +didFailWithError:(NSError *) error { + QMetaObject::invokeMethod(self.parent, "extLoaderFailure"); + Q_ARG(QString, QString::fromNSString(error.localizedDescription)); +} + +- (void) requestNeedsUserApproval:(OSSystemExtensionRequest *) request { + QMetaObject::invokeMethod(self.parent, "extNeedsApproval"); +} + +- (OSSystemExtensionReplacementAction) request:(OSSystemExtensionRequest *) request + actionForReplacingExtension:(OSSystemExtensionProperties *) existing + withExtension:(OSSystemExtensionProperties *) ext { + logger.warning() << "extension replacement action:" << existing.bundleVersion + << "->" << ext.bundleVersion; + return OSSystemExtensionReplacementActionReplace; +} + +- (void) request:(OSSystemExtensionRequest *) request +didFinishWithResult:(OSSystemExtensionRequestResult) result { + QMetaObject::invokeMethod(self.parent, "extLoaderSuccess", Q_ARG(int, result)); +} + +@end + diff --git a/src/platforms/macos/macosextensionloader.h b/src/platforms/macos/macosextensionloader.h deleted file mode 100644 index 3c1789893a..0000000000 --- a/src/platforms/macos/macosextensionloader.h +++ /dev/null @@ -1,17 +0,0 @@ -/* 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/. */ - -#ifndef MACOSEXTENSIONLOADER_H -#define MACOSEXTENSIONLOADER_H - -#import -#import -#import - -@interface MacosExtensionLoader : NSObject -@property(readonly) NSString* identifier; -@property(retain) NETransparentProxyManager* manager; -@end - -#endif // MACOSEXTENSIONLOADER_H diff --git a/src/platforms/macos/macosextensionloader.mm b/src/platforms/macos/macosextensionloader.mm deleted file mode 100644 index 74b7dc8a25..0000000000 --- a/src/platforms/macos/macosextensionloader.mm +++ /dev/null @@ -1,110 +0,0 @@ -/* 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 "macosextensionloader.h" - -#include "logger.h" - -#import -#import -#import - -namespace { -Logger logger("MacosExtensionLoader"); -} // namespace - -@implementation MacosExtensionLoader - -- (NSString*) identifier { - NSString* appId = [[NSBundle mainBundle] bundleIdentifier]; - return [appId stringByAppendingString:@".network-extension"]; -} - -- (id)init { - self = [super init]; - self.manager = nullptr; - return self; -} - -- (void) request:(OSSystemExtensionRequest *) request -didFailWithError:(NSError *) error { - logger.warning() << "activation request failed:" << error.localizedDescription; -} - -- (void) requestNeedsUserApproval:(OSSystemExtensionRequest *) request { - logger.warning() << "activation request needs user approval"; -} - -- (OSSystemExtensionReplacementAction) request:(OSSystemExtensionRequest *) request - actionForReplacingExtension:(OSSystemExtensionProperties *) existing - withExtension:(OSSystemExtensionProperties *) ext { - logger.warning() << "activation replacement action:" << existing.bundleVersion - << "->" << ext.bundleVersion; - return OSSystemExtensionReplacementActionReplace; -} - -- (void) request:(OSSystemExtensionRequest *) request -didFinishWithResult:(OSSystemExtensionRequestResult) result { - logger.info() << "activation request complete:" << result; - [self setupManager:^(NSError* error){ - if (error != nil) { - logger.info() << "proxy setup failed:" << error.localizedDescription; - } else { - logger.info() << "proxy setup complete"; - } - }]; -} - -- (void) setupManager:(void (^)(NSError * error)) completionHandler { - [NETransparentProxyManager loadAllFromPreferencesWithCompletionHandler:^(NSArray* managers, NSError* err){ - if (err != nil) { - logger.warning() << "proxy manager load failed:" - << err.localizedDescription; - completionHandler(err); - return; - } - - // Check if an existing manager can be used. - for (NETransparentProxyManager* mgr in managers) { - NETunnelProviderProtocol* proto = - static_cast([mgr protocolConfiguration]); - if ([proto.providerBundleIdentifier isEqualToString:self.identifier]) { - logger.info() << "proxy manager found for:" - << proto.providerBundleIdentifier; - self.manager = mgr; - break; - } - } - // Otherwise - create a new manager. - if (self.manager == nil) { - self.manager = [NETransparentProxyManager new]; - self.manager.localizedDescription = @"Mozilla VPN Split Tunnel"; - logger.info() << "proxy manager created for:" << self.identifier; - } - - // Update the tunnel configuration. - auto protocol = [NETunnelProviderProtocol new]; - protocol.providerBundleIdentifier = self.identifier; - protocol.serverAddress = @"127.0.0.1"; - self.manager.protocolConfiguration = protocol; - - // Enable the manager and sync preferences - self.manager.enabled = true; - [self.manager saveToPreferencesWithCompletionHandler:^(NSError* saveErr){ - if (saveErr != nil) { - logger.debug() << "proxy prefs setup failed:" - << saveErr.localizedDescription; - } - [self.manager loadFromPreferencesWithCompletionHandler:^(NSError* loadErr){ - if (loadErr != nil) { - logger.debug() << "proxy prefs load failed:" - << loadErr.localizedDescription; - } - completionHandler(nil); - }]; - }]; - }]; -} - -@end From 98bec8a85b849f4de884b00e60e1142eae4a51ca Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 22 Apr 2026 14:04:57 -0700 Subject: [PATCH 07/31] Get wireguard tunnel bringup to be somewhat stable --- .../VPNSplitTunnelProvider.mm | 15 +++ macos/networkextension/wireguardtunnel.h | 2 +- macos/networkextension/wireguardtunnel.mm | 116 +++++++++++------- .../macos/macosextensioncontroller.h | 7 +- .../macos/macosextensioncontroller.mm | 56 ++++++++- 5 files changed, 146 insertions(+), 50 deletions(-) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index 36daa4d649..b4568bc42e 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -464,6 +464,21 @@ - (void)handleAppMessage:(NSData *)messageData } [msg finishDecoding]; + // Wireguard Tunnel messages + if ([action isEqualToString:@"status"]) { + NSError* err; + NSData* reply = [NSKeyedArchiver archivedDataWithRootObject:self.wireguard.status + requiringSecureCoding:YES + error:&err]; + if (err == nil) { + [VPNSplitTunnelProvider sendAppError:err completionHandler:completionHandler]; + } else { + [VPNSplitTunnelProvider sendAppResponse:reply completionHandler:completionHandler]; + } + return; + } + + // Application exclusion messages. if ([action isEqualToString: @"clear"]) { [self.vpnDisabledApps removeAllObjects]; } else if ([action isEqualToString: @"add"]) { diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h index e32a267097..f13ebb3c58 100644 --- a/macos/networkextension/wireguardtunnel.h +++ b/macos/networkextension/wireguardtunnel.h @@ -29,7 +29,7 @@ - (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; @property (nonatomic) NSUInteger mtu; -@property (strong, readonly, getter=getStatus) WireguardStats* stats; +@property (strong, readonly, getter=getStatus) WireguardStats* status; @property (strong) nw_connection_t connection; @property (strong) nw_interface_t virtualInterface; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 5d697314a9..3a1d98f5ba 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -44,11 +44,16 @@ @implementation WireguardTunnel { CFRunLoopTimerRef m_timer; struct timespec m_lastHandshake; + struct timespec m_handshakeTimeout; + + // The completion handler to run on initial handshake or timeout. + void (^m_completionHandler)(NSError *error); } constexpr const int WG_PACKET_OVERHEAD = 32; constexpr const int WG_MTU_OVERHEAD = 80; constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; +constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; static void wgLog(const char* msg) { NSLog(@"wg: %s", msg); @@ -99,10 +104,10 @@ - (id)init { - (void) startTunnelWithOptions:(InterfaceConfig *)options completionHandler:(void (^)(NSError *error)) completionHandler { + m_completionHandler = completionHandler; m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (m_tunfd < 0) { - [self shutdownTunnel:errorFromErrno(errno, @"tunnel creation failed") - completionHandler:completionHandler]; + [self shutdownTunnel:@"tunnel creation failed" withErrno:errno]; return; } @@ -110,8 +115,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; int err = ioctl(m_tunfd, CTLIOCGINFO, &info); if (err < 0) { - [self shutdownTunnel:errorFromErrno(errno, @"kernel utun lookup failed") - completionHandler:completionHandler]; + [self shutdownTunnel:@"kernel utun lookup failed" withErrno:errno]; return; } struct sockaddr_ctl addr = {}; @@ -122,8 +126,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options addr.sc_unit = 0; err = connect(m_tunfd, (struct sockaddr*)&addr, sizeof(addr)); if (err < 0) { - [self shutdownTunnel:errorFromErrno(errno, @"kernel utun connect failed") - completionHandler:completionHandler]; + [self shutdownTunnel:@"kernel utun connect failed" withErrno:errno]; return; } @@ -133,8 +136,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, &ifnamesize); if (err < 0) { - [self shutdownTunnel:errorFromErrno(errno, @"utun name loookup failed") - completionHandler:completionHandler]; + [self shutdownTunnel:@"utun name loookup failed" withErrno:errno]; return; } int ifindex = if_nametoindex(ifr.ifr_name); @@ -142,35 +144,31 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Assign addresses if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { - [self shutdownTunnel:err completionHandler:completionHandler]; + [self shutdownTunnel:err]; return; } if (NSError *err = [self setTunnelAddress:options.deviceIpv6Addr]) { - [self shutdownTunnel:err completionHandler:completionHandler]; + [self shutdownTunnel:err]; return; } // Set a base MTU, it will get updated later. ifr.ifr_mtu = self.mtu; if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { - [self shutdownTunnel:errorFromErrno(errno, @"failed to set mtu") - completionHandler:completionHandler]; + [self shutdownTunnel:@"failed to set mtu" withErrno:errno]; return; } - // Bring the device up. err = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); if (err != 0) { - [self shutdownTunnel:errorFromErrno(errno, @"failed to get interface flags") - completionHandler:completionHandler]; + [self shutdownTunnel:@"failed to get interface flags" withErrno:errno]; return; } ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); err = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); if (err != 0) { - [self shutdownTunnel:errorFromErrno(errno, @"failed to set device up") - completionHandler:completionHandler]; + [self shutdownTunnel:@"failed to set device up" withErrno:errno]; return; } @@ -207,17 +205,17 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options 300, // Keepalive period index % (1U << 24)); + m_completionHandler = completionHandler; nw_connection_set_state_changed_handler(self.connection, ^(nw_connection_state_t state, nw_error_t err) { - // TODO: Implement Me! if (err) { CFErrorRef cfError = nw_error_copy_cf_error(err); NSLog(@"vpn socket error: %@", (__bridge NSError*)cfError); - completionHandler((__bridge NSError*)cfError); + [self shutdownTunnel:(__bridge NSError*)cfError]; CFRelease(cfError); } else if (state == nw_connection_state_cancelled || state == nw_connection_state_failed) { NSLog(@"vpn socket closed"); - [self cancelTunnelWithError:nil]; + [self shutdownTunnel:nil]; } else if (state != nw_connection_state_ready) { NSLog(@"vpn socket state %d", state); } else { @@ -237,9 +235,11 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options - (void) stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)()) completionHandler { - [self shutdownTunnel:nil completionHandler:^(NSError*error){ + m_completionHandler = ^(NSError *error){ completionHandler(); - }]; + }; + + [self shutdownTunnel:nil]; } - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { @@ -305,10 +305,14 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { - (void) renegotiate { NSLog(@"wireguard renegotiate"); UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); - NSMutableData* buffer = [NSMutableData dataWithLength:WG_MAX_HANDSHAKE_SIZE]; + struct wireguard_result r; r = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + // Set a timeout for the handshake to finish. + clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); + m_handshakeTimeout.tv_sec += WG_MAX_HANDSHAKE_TIMEOUT; + dispatch_data_t data = dispatch_data_create(handshake, r.size, dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_FREE); @@ -316,6 +320,21 @@ - (void) renegotiate { } - (void) handleTimer { + // Check for a handshake timeout. + if (m_handshakeTimeout.tv_sec && m_handshakeTimeout.tv_nsec) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > m_handshakeTimeout.tv_sec) { + [self shutdownTunnel:@"handshake timeout" withErrno:ETIMEDOUT]; + return; + } else if (now.tv_sec < m_handshakeTimeout.tv_sec) { + // It has not timed out. + } else if (now.tv_nsec > m_handshakeTimeout.tv_nsec) { + [self shutdownTunnel:@"handshake timeout" withErrno:ETIMEDOUT]; + return; + } + } + UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); struct wireguard_result r; r = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); @@ -342,7 +361,6 @@ - (void) handleOutbound:(NSData*)packet [plaintext increaseLengthBy:16 - tail]; } - // Encrypt the packet. size_t length = plaintext.length + WG_PACKET_OVERHEAD; uint8_t* ciphertext = (uint8_t*)malloc(length); @@ -357,8 +375,8 @@ - (void) handleOutbound:(NSData*)packet } - (void) handleInbound { - nw_connection_receive(self.connection, 1, UINT16_MAX, - ^(dispatch_data_t data, nw_content_context_t ctx, bool done, nw_error_t err){ + nw_connection_receive_message(self.connection, + ^(dispatch_data_t data, nw_content_context_t ctx, bool done, nw_error_t err){ if (err) { CFErrorRef cfError = nw_error_copy_cf_error(err); NSLog(@"recv error: %@", (__bridge NSError *)cfError); @@ -366,7 +384,9 @@ - (void) handleInbound { return; } if (!data) { - //[self closeConnection:nil completionHandler:completionHandler]; + // No data? That was kinda weird - oh well, try again. + NSLog(@"recv empty"); + [self handleInbound]; return; } @@ -374,7 +394,6 @@ - (void) handleInbound { const void *ciphertext; dispatch_data_t __unused map = dispatch_data_create_map(data, &ciphertext, &length); uint8_t* plaintext = (uint8_t*)malloc(length); - NSLog(@"wireguard recv: %zu", length); // Decrypt the wireguard packet. struct wireguard_result r; @@ -389,6 +408,15 @@ - (void) handleInbound { memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); } else if (wgStats.time_since_last_handshake < 5) { clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); + + // Clear the handshake timeout + memset(&m_handshakeTimeout, 0, sizeof(m_handshakeTimeout)); + + // The conneciton is now up. + if (m_completionHandler) { + m_completionHandler(nil); + m_completionHandler = nil; + } } } @@ -396,6 +424,9 @@ - (void) handleInbound { dispatch_get_main_queue(), DISPATCH_DATA_DESTRUCTOR_FREE); [self handleWireguard:(int)r.op withData:packet]; + + // Keep going to receive more data. + [self handleInbound]; }); } @@ -406,7 +437,6 @@ - (void) handleWireguard:(int)op break; case WRITE_TO_NETWORK: - NSLog(@"wireguard send: %zu", dispatch_data_get_size(data)); nw_connection_send(self.connection, data, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, ^(nw_error_t error) { @@ -414,8 +444,6 @@ - (void) handleWireguard:(int)op CFErrorRef cfError = nw_error_copy_cf_error(error); NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); CFRelease(cfError); - } else { - NSLog(@"wireguard send okay"); } }); break; @@ -427,7 +455,6 @@ - (void) handleWireguard:(int)op case WRITE_TO_TUNNEL_IPV4: [[fallthrough]]; case WRITE_TO_TUNNEL_IPV6: - NSLog(@"wireguard send: %zu", dispatch_data_get_size(data)); NSData* packet = (NSData*)data; uint32_t header = (op == WRITE_TO_TUNNEL_IPV6) ? htonl(AF_INET6) : htonl(AF_INET); const struct iovec iov[2] = { @@ -440,8 +467,11 @@ - (void) handleWireguard:(int)op } } -- (void)shutdownTunnel:(NSError*)error - completionHandler:(void (^)(NSError *error)) completionHandler { +- (void)shutdownTunnel:(NSString*)desc withErrno:(int)code { + [self shutdownTunnel:errorFromErrno(code, desc)]; +} + +- (void)shutdownTunnel:(NSError*)error { if (error) { NSLog(@"wireguard shutdown: %@", error); } else { @@ -456,17 +486,18 @@ - (void)shutdownTunnel:(NSError*)error m_timer = nil; } + if (m_socket) { + CFSocketInvalidate(m_socket); + CFRelease(m_socket); + m_socket = nil; + } + if (m_source) { CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); CFRelease(m_source); m_source = nil; } - if (m_socket) { - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - } - if (m_tunfd >= 0) { close(m_tunfd); m_tunfd = -1; @@ -477,13 +508,14 @@ - (void)shutdownTunnel:(NSError*)error m_wireguard = nil; } - if (completionHandler) { - completionHandler(error); + if (m_completionHandler) { + m_completionHandler(error); + m_completionHandler = nil; } } - (void)cancelTunnelWithError:(NSError*)error { - [self shutdownTunnel:error completionHandler:nil]; + [self shutdownTunnel:error]; } - (WireguardStats*)getStatus { diff --git a/src/platforms/macos/macosextensioncontroller.h b/src/platforms/macos/macosextensioncontroller.h index 4758738ecf..2cac719b1c 100644 --- a/src/platforms/macos/macosextensioncontroller.h +++ b/src/platforms/macos/macosextensioncontroller.h @@ -9,6 +9,7 @@ #include +Q_FORWARD_DECLARE_OBJC_CLASS(MacOSExtensionDelegate); Q_FORWARD_DECLARE_OBJC_CLASS(NETransparentProxyManager); Q_FORWARD_DECLARE_OBJC_CLASS(NETunnelProviderSession); @@ -34,12 +35,16 @@ class MacOSExtensionController final : public ControllerImpl { void extLoaderSuccess(int result); void extLoaderFailure(const QString& reason); void extNeedsApproval(); + void extEnabledChange(bool value); + void extStatusChange(int status); private: static NSString* extIdentifier(); private: - void* m_delegate = nullptr; + QString m_serverPublicKey; + + MacOSExtensionDelegate* m_delegate = nullptr; NETransparentProxyManager* m_manager = nullptr; NETunnelProviderSession* m_session = nullptr; }; diff --git a/src/platforms/macos/macosextensioncontroller.mm b/src/platforms/macos/macosextensioncontroller.mm index fb65264a3a..c9577eb67c 100644 --- a/src/platforms/macos/macosextensioncontroller.mm +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -17,6 +17,8 @@ @interface MacOSExtensionDelegate : NSObject @property MacOSExtensionController* parent; - (id)initWithObject:(MacOSExtensionController*)controller; +- (void)notifyEnabledChanged:(NSNotification*)notify; +- (void)notifyStatusChanged:(NSNotification*)notify; @end namespace { @@ -26,11 +28,11 @@ - (id)initWithObject:(MacOSExtensionController*)controller; MacOSExtensionController::MacOSExtensionController() : ControllerImpl() { // Create the system extension loader delegate. m_delegate = [[MacOSExtensionDelegate alloc] initWithObject:this]; - [static_cast(m_delegate) retain]; + [m_delegate retain]; } MacOSExtensionController::~MacOSExtensionController() { - [static_cast(m_delegate) release]; + [m_delegate release]; } void MacOSExtensionController::initialize(const Device* device, const Keys* keys) { @@ -40,7 +42,7 @@ - (id)initWithObject:(MacOSExtensionController*)controller; OSSystemExtensionRequest* req = [OSSystemExtensionRequest activationRequestForExtension: extIdentifier() queue: queue]; - req.delegate = static_cast(m_delegate); + req.delegate = m_delegate; // Start the request logger.debug() << "activation request started:" << req.identifier; @@ -90,6 +92,13 @@ - (id)initWithObject:(MacOSExtensionController*)controller; protocol.serverAddress = @"127.0.0.1"; m_manager.protocolConfiguration = protocol; + // Register the delegate to receive updates when the extension state is changed. + NSNotificationCenter* notify = [NSNotificationCenter defaultCenter]; + [notify addObserver:m_delegate + selector:@selector(notifyEnabledChanged:) + name:NEVPNConfigurationChangeNotification + object:m_manager]; + // Enable the manager and sync preferences m_manager.enabled = true; [m_manager saveToPreferencesWithCompletionHandler:^(NSError* saveErr){ @@ -102,7 +111,17 @@ - (id)initWithObject:(MacOSExtensionController*)controller; logger.debug() << "proxy prefs load failed:" << loadErr.localizedDescription; } - emit initialized(true, false, QDateTime()); + NEVPNConnection* conn = m_manager.connection; + [notify addObserver:m_delegate + selector:@selector(notifyStatusChanged:) + name:NEVPNStatusDidChangeNotification + object:conn]; + + if (conn.status == NEVPNStatusConnected) { + emit initialized(true, true, QDateTime::fromCFDate((CFDateRef)conn.connectedDate)); + } else { + emit initialized(true, false, QDateTime()); + } }]; }]; }]; @@ -117,6 +136,20 @@ - (id)initWithObject:(MacOSExtensionController*)controller; emit permissionRequired(); } +void MacOSExtensionController::extEnabledChange(bool enabled) { + logger.warning() << "activation enable changed:" << enabled; +} + +void MacOSExtensionController::extStatusChange(int status) { + logger.warning() << "connection status changed:" << status; + if (status == NEVPNStatusConnected) { + emit connected(m_serverPublicKey); + } + if (status == NEVPNStatusDisconnected) { + emit disconnected(); + } +} + void MacOSExtensionController::activate(const InterfaceConfig& config, Controller::Reason reason) { Q_UNUSED(reason); @@ -127,6 +160,9 @@ - (id)initWithObject:(MacOSExtensionController*)controller; return; } + // Save the public key for signal emissions. + m_serverPublicKey = config.m_serverPublicKey; + // Serialize the interface configuration. NSMutableDictionary* options = [NSMutableDictionary dictionary]; [options setObject:config.m_privateKey.toNSString() forKey:@"privateKey"]; @@ -159,7 +195,6 @@ - (id)initWithObject:(MacOSExtensionController*)controller; } else { // Save the session and retain it. m_session = [session retain]; - emit connected(config.m_serverPublicKey); } } @@ -225,5 +260,14 @@ - (void) request:(OSSystemExtensionRequest *) request QMetaObject::invokeMethod(self.parent, "extLoaderSuccess", Q_ARG(int, result)); } -@end +- (void)notifyEnabledChanged:(NSNotification*)notify { + bool enabled = static_cast(notify.object).enabled; + QMetaObject::invokeMethod(self.parent, "extEnabledChange", Q_ARG(bool, enabled)); +} +- (void)notifyStatusChanged:(NSNotification*)notify { + NEVPNStatus status = static_cast(notify.object).status; + QMetaObject::invokeMethod(self.parent, "extStatusChange", Q_ARG(int, status)); +} + +@end From b9033a1449f9058620877767377d0681985bbefc Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 22 Apr 2026 16:06:37 -0700 Subject: [PATCH 08/31] Implement routing table setup for allowed IP ranges --- .../VPNSplitTunnelProvider.mm | 100 +++++++++++------- macos/networkextension/interfaceconfig.h | 7 ++ macos/networkextension/interfaceconfig.mm | 54 +++++++++- macos/networkextension/routemanager.h | 4 +- macos/networkextension/routemanager.mm | 26 +++-- macos/networkextension/wireguardtunnel.mm | 13 +-- .../macos/macosextensioncontroller.mm | 7 ++ 7 files changed, 158 insertions(+), 53 deletions(-) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index b4568bc42e..e379324e48 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -255,7 +255,27 @@ - (void)startProxyWithOptions:(NSDictionary *)options } else { NSLog(@"settings applied"); [weakSelf.wireguard startTunnelWithOptions:weakSelf.config - completionHandler:completionHandler]; + completionHandler:^(NSError* wgError){ + if (wgError != nil) { + completionHandler(wgError); + return; + } + + // Configure routes into the tunnel interface. + // Note that the default route should set RTF_IFSCOPE so that we + // leave the real default route untouched. + for (RoutePrefix* prefix in weakSelf.config.routes) { + [weakSelf.routeManager rtmSendRoute:RTM_ADD + toDestination:nw_endpoint_get_address(prefix.destination) + withPrefix:prefix.prefixLength + viaInterface:weakSelf.wireguard.virtualInterface + withGateway:nil + andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; + } + + // Success + completionHandler(nil); + }]; } }]; } @@ -272,12 +292,11 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_len = sizeof(sin); - NSData* dst = [NSData dataWithBytes:&sin length:sizeof(sin)]; [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dst + toDestination:(struct sockaddr*)&sin withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv4Interface) + viaInterface:self.ipv4Interface withGateway:nil andFlags:RTF_IFSCOPE]; @@ -290,12 +309,11 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_len = sizeof(sin6); - NSData* dst = [NSData dataWithBytes:&sin6 length:sizeof(sin6)]; [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dst + toDestination:(struct sockaddr*)&sin6 withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv6Interface) + viaInterface:self.ipv6Interface withGateway:nil andFlags:RTF_IFSCOPE]; @@ -514,7 +532,6 @@ + (void)sendAppError:(NSError*) error - (void)defaultRouteChanged:(int)family viaInterface:(nw_interface_t)interface withGateway:(NSData*)gateway { - int action = RTM_ADD; int ifindex = interface ? nw_interface_get_index(interface) : 0; if (family == AF_INET) { @@ -522,47 +539,52 @@ - (void)defaultRouteChanged:(int)family memset(&dst, 0, sizeof(dst)); dst.sin_family = AF_INET; dst.sin_len = sizeof(dst); - NSData* dstAddr = [NSData dataWithBytes:&dst length:sizeof(dst)]; if (interface) { + int action = RTM_ADD; NSLog(@"default ipv4 route via %s", nw_interface_get_name(interface)); + if (!self.ipv4Interface) { action = RTM_ADD; } else if (ifindex == nw_interface_get_index(self.ipv4Interface)) { action = RTM_CHANGE; } else { + // Default route has changed interface - delete the old clone. action = RTM_ADD; [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dstAddr + toDestination:(struct sockaddr*)&dst withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv4Interface) + viaInterface:self.ipv4Interface withGateway:nil andFlags:RTF_IFSCOPE]; } + + // Add or update the cloned default route. + [self.routeManager rtmSendRoute:action + toDestination:(struct sockaddr*)&dst + withPrefix:0 + viaInterface:interface + withGateway:gateway + andFlags:RTF_IFSCOPE]; } else if (self.ipv4Interface) { NSLog(@"default ipv4 route lost"); - action = RTM_DELETE; - ifindex = nw_interface_get_index(self.ipv4Interface); + [self.routeManager rtmSendRoute:RTM_DELETE + toDestination:(struct sockaddr*)&dst + withPrefix:0 + viaInterface:self.ipv4Interface + withGateway:gateway + andFlags:RTF_IFSCOPE]; } - // Update the cloned IPv4 default route. self.ipv4Interface = interface; - if (ifindex != 0) { - [self.routeManager rtmSendRoute:action - toDestination:dstAddr - withPrefix:0 - viaInterface:ifindex - withGateway:gateway - andFlags:RTF_IFSCOPE]; - } } else if (family == AF_INET6) { struct sockaddr_in6 dst; memset(&dst, 0, sizeof(dst)); dst.sin6_family = AF_INET6; dst.sin6_len = sizeof(dst); - NSData* dstAddr = [NSData dataWithBytes:&dst length:sizeof(dst)]; if (interface) { + int action = RTM_ADD; NSLog(@"default ipv6 route via %s", nw_interface_get_name(interface)); if (!self.ipv6Interface) { @@ -571,29 +593,33 @@ - (void)defaultRouteChanged:(int)family action = RTM_CHANGE; } else { action = RTM_ADD; + // Default route has changed interface - delete the old clone. [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dstAddr - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv6Interface) + toDestination:(struct sockaddr*)&dst + withPrefix:0 + viaInterface:self.ipv6Interface withGateway:nil - andFlags:RTF_IFSCOPE]; + andFlags:RTF_IFSCOPE]; } - } else if (self.ipv6Interface) { - NSLog(@"default ipv6 route lost"); - action = RTM_DELETE; - ifindex = nw_interface_get_index(self.ipv6Interface); - } - // Update the cloned IPv6 default route. - self.ipv6Interface = interface; - if (ifindex != 0) { + // Add or update the cloned default route. [self.routeManager rtmSendRoute:action - toDestination:dstAddr + toDestination:(struct sockaddr*)&dst + withPrefix:0 + viaInterface:interface + withGateway:gateway + andFlags:RTF_IFSCOPE]; + } else if (self.ipv6Interface) { + NSLog(@"default ipv6 route lost"); + [self.routeManager rtmSendRoute:RTM_DELETE + toDestination:(struct sockaddr*)&dst withPrefix:0 - viaInterface:ifindex + viaInterface:interface withGateway:gateway andFlags:RTF_IFSCOPE]; } + + self.ipv6Interface = interface; } } diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h index 003246f3b4..ee52d6fcc4 100644 --- a/macos/networkextension/interfaceconfig.h +++ b/macos/networkextension/interfaceconfig.h @@ -5,6 +5,11 @@ #import #import +@interface RoutePrefix : NSObject +@property (strong) nw_endpoint_t destination; +@property NSUInteger prefixLength; +@end + @interface InterfaceConfig : NSObject + (id)parseFromDict:(NSDictionary *)dict; @@ -19,6 +24,8 @@ @property (strong) nw_endpoint_t serverIpv4Addr; @property (strong) nw_endpoint_t serverIpv6Addr; +@property (strong) NSArray* routes; + @property (strong, readonly) NSDictionary* dict; @end diff --git a/macos/networkextension/interfaceconfig.mm b/macos/networkextension/interfaceconfig.mm index f3372e0a81..65ca263be6 100644 --- a/macos/networkextension/interfaceconfig.mm +++ b/macos/networkextension/interfaceconfig.mm @@ -10,6 +10,10 @@ #include #include +@implementation RoutePrefix +// Nothing to do here +@end + @implementation InterfaceConfig - (NSString*)findString:(NSString*)key { @@ -95,7 +99,7 @@ + (id)parseFromDict:(NSDictionary *)dict { config.serverIpv4Addr = nw_endpoint_create_address((struct sockaddr*)&sin); } - NSString* serverIpv6Addr = [dict objectForKey:@"serverIpv6AddrIn"]; + NSString* serverIpv6Addr = [config findString:@"serverIpv6AddrIn"]; if (serverIpv6Addr) { struct sockaddr_in6 sin6; memset(&sin6, 0, sizeof(sin6)); @@ -109,6 +113,54 @@ + (id)parseFromDict:(NSDictionary *)dict { config.serverIpv6Addr = nw_endpoint_create_address((struct sockaddr*)&sin6); } + // Parse the allowed IP address ranges. + NSObject* rangesObject = [config.dict objectForKey:@"routes"]; + NSMutableArray* routes = [NSMutableArray new]; + if ([rangesObject isKindOfClass:[NSArray class]]) { + NSArray* list = (NSArray*)rangesObject; + for (NSString* rangeString in list) { + NSArray* split = [rangeString componentsSeparatedByString:@"/"]; + NSString* dest = split[0]; + RoutePrefix* prefix = [RoutePrefix new]; + + if ([dest containsString:@":"]) { + // IPv6 address + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_port = 0; // Not used. + if (!inet_pton(AF_INET6, dest.UTF8String, &sin6.sin6_addr.s6_addr)) { + return nil; + } + prefix.destination = nw_endpoint_create_address((struct sockaddr*)&sin6); + prefix.prefixLength = 128; + } else { + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_port = 0; // Not used. + if (!inet_pton(AF_INET, dest.UTF8String, &sin.sin_addr.s_addr)) { + return nil; + } + prefix.destination = nw_endpoint_create_address((struct sockaddr*)&sin); + prefix.prefixLength = 32; + } + + if (split.count > 1) { + NSInteger i = split[1].integerValue; + if ((i < 0) || (i > prefix.prefixLength)) { + return nil; + } + prefix.prefixLength = i; + } + + [routes addObject:prefix]; + } + } + config.routes = [NSArray arrayWithArray:routes]; + return config; } diff --git a/macos/networkextension/routemanager.h b/macos/networkextension/routemanager.h index 8123bf72a3..45a0336bf3 100644 --- a/macos/networkextension/routemanager.h +++ b/macos/networkextension/routemanager.h @@ -19,9 +19,9 @@ struct sockaddr; - (void)startWithDelegate:(NSObject*)delegate; - (void)rtmSendRoute:(int)action - toDestination:(NSData*)dst + toDestination:(const struct sockaddr*)dst withPrefix:(unsigned int)plen - viaInterface:(unsigned int)ifindex + viaInterface:(nw_interface_t)ifindex withGateway:(NSData*)gateway andFlags:(int)flags; @end diff --git a/macos/networkextension/routemanager.mm b/macos/networkextension/routemanager.mm index 579fd885c1..0ecc8a72b9 100644 --- a/macos/networkextension/routemanager.mm +++ b/macos/networkextension/routemanager.mm @@ -429,9 +429,9 @@ - (void)rtmFetchRoutes:(int)family { } - (void) rtmSendRoute:(int)action - toDestination:(NSData*)dst + toDestination:(const struct sockaddr*)dst withPrefix:(unsigned int)plen - viaInterface:(unsigned int)ifindex + viaInterface:(nw_interface_t)interface withGateway:(NSData*)gateway andFlags:(int)flags { constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + @@ -443,7 +443,7 @@ - (void) rtmSendRoute:(int)action rtm->rtm_msglen = sizeof(struct rt_msghdr); rtm->rtm_version = RTM_VERSION; rtm->rtm_type = action; - rtm->rtm_index = ifindex; + rtm->rtm_index = nw_interface_get_index(interface); rtm->rtm_flags = flags | RTF_STATIC | RTF_UP; rtm->rtm_addrs = 0; rtm->rtm_pid = 0; @@ -453,8 +453,7 @@ - (void) rtmSendRoute:(int)action memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx)); // Append RTA_DST - const struct sockaddr* dstAddr = (const struct sockaddr*)dst.bytes; - rtmAppendAddr(rtm, rtm_max_size, RTA_DST, dstAddr); + rtmAppendAddr(rtm, rtm_max_size, RTA_DST, dst); // Append RTA_GATEWAY if (gateway != nullptr) { @@ -463,10 +462,23 @@ - (void) rtmSendRoute:(int)action rtm->rtm_flags |= RTF_GATEWAY; } rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, gw); + } else if (action != RTM_DELETE) { + const char* ifname = nw_interface_get_name(interface); + struct sockaddr_dl datalink; + memset(&datalink, 0, sizeof(datalink)); + datalink.sdl_family = AF_LINK; + datalink.sdl_len = offsetof(struct sockaddr_dl, sdl_data) + strlen(ifname); + datalink.sdl_index = nw_interface_get_index(interface); + datalink.sdl_type = IFT_OTHER; + datalink.sdl_nlen = strlen(ifname); + datalink.sdl_alen = 0; + datalink.sdl_slen = 0; + memcpy(&datalink.sdl_data, ifname, datalink.sdl_nlen); + rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, (struct sockaddr*)&datalink); } // Append RTA_NETMASK - if (dstAddr->sa_family == AF_INET6) { + if (dst->sa_family == AF_INET6) { struct sockaddr_in6 mask; memset(&mask, 0, sizeof(mask)); mask.sin6_family = AF_INET6; @@ -476,7 +488,7 @@ - (void) rtmSendRoute:(int)action mask.sin6_addr.s6_addr[plen / 8] = 0xFF ^ (0xFF >> (plen % 8)); } rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); - } else if (dstAddr->sa_family == AF_INET) { + } else if (dst->sa_family == AF_INET) { struct sockaddr_in mask; memset(&mask, 0, sizeof(mask)); mask.sin_family = AF_INET; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 3a1d98f5ba..5dfccff968 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -197,8 +197,6 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options self.connection = nw_connection_create(options.serverIpv4Addr, params); nw_connection_set_queue(self.connection, dispatch_get_main_queue()); - // The tunnel username should contain the base64-encoded server public key and - // the password should hold the device private key. m_wireguard = new_tunnel(options.privateKey.UTF8String, options.serverPublicKey.UTF8String, nil, // Preshared key @@ -383,10 +381,10 @@ - (void) handleInbound { CFRelease(cfError); return; } - if (!data) { - // No data? That was kinda weird - oh well, try again. + + if (data == nil) { + // This may be nil if the message or stream is complete. NSLog(@"recv empty"); - [self handleInbound]; return; } @@ -477,7 +475,10 @@ - (void)shutdownTunnel:(NSError*)error { } else { NSLog(@"wireguard shutdown"); } - self.connection = nil; + if (self.connection) { + nw_connection_cancel(self.connection); + self.connection = nil; + } self.virtualInterface = nil; if (m_timer) { diff --git a/src/platforms/macos/macosextensioncontroller.mm b/src/platforms/macos/macosextensioncontroller.mm index c9577eb67c..6b9e065362 100644 --- a/src/platforms/macos/macosextensioncontroller.mm +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -175,6 +175,13 @@ - (void)notifyStatusChanged:(NSNotification*)notify; [options setObject:config.m_serverIpv6Gateway.toNSString() forKey:@"serverIpv6Gateway"]; [options setObject:[NSNumber numberWithInt:config.m_serverPort] forKey:@"serverPort"]; + NSMutableArray* ipAddressRanges = + [NSMutableArray arrayWithCapacity:config.m_allowedIPAddressRanges.length()]; + for (const IPAddress& range : config.m_allowedIPAddressRanges) { + [ipAddressRanges addObject:range.toString().toNSString()]; + } + [options setObject:ipAddressRanges forKey:@"routes"]; + // Serialize the excluded application list. NSMutableArray* vpnDisabledApps = [NSMutableArray arrayWithCapacity:config.m_vpnDisabledApps.length()]; From 2a1319911e73e1c22bbea3b0aecfd7ec97c001ab Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 22 Apr 2026 16:30:51 -0700 Subject: [PATCH 09/31] Implement a parser method to the RoutePrefix class --- .../VPNSplitTunnelProvider.mm | 2 +- macos/networkextension/interfaceconfig.h | 7 +- macos/networkextension/interfaceconfig.mm | 89 +++++++++++-------- macos/networkextension/wireguardtunnel.mm | 1 - 4 files changed, 57 insertions(+), 42 deletions(-) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index e379324e48..9043ea130f 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -266,7 +266,7 @@ - (void)startProxyWithOptions:(NSDictionary *)options // leave the real default route untouched. for (RoutePrefix* prefix in weakSelf.config.routes) { [weakSelf.routeManager rtmSendRoute:RTM_ADD - toDestination:nw_endpoint_get_address(prefix.destination) + toDestination:prefix.destination withPrefix:prefix.prefixLength viaInterface:weakSelf.wireguard.virtualInterface withGateway:nil diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h index ee52d6fcc4..ffac32a394 100644 --- a/macos/networkextension/interfaceconfig.h +++ b/macos/networkextension/interfaceconfig.h @@ -6,8 +6,11 @@ #import @interface RoutePrefix : NSObject -@property (strong) nw_endpoint_t destination; -@property NSUInteger prefixLength; + ++ (id)parseRoute:(NSString *)dict; + +@property (readonly, getter=getDestination) const struct sockaddr* destination; +@property (readonly) NSUInteger prefixLength; @end @interface InterfaceConfig : NSObject diff --git a/macos/networkextension/interfaceconfig.mm b/macos/networkextension/interfaceconfig.mm index 65ca263be6..af2e32a869 100644 --- a/macos/networkextension/interfaceconfig.mm +++ b/macos/networkextension/interfaceconfig.mm @@ -10,8 +10,54 @@ #include #include -@implementation RoutePrefix -// Nothing to do here +@implementation RoutePrefix { + struct sockaddr_storage m_storage; +} + ++ (id)parseRoute:(NSString*)routeString { + RoutePrefix* route = [RoutePrefix new]; + + NSArray* split = [routeString componentsSeparatedByString:@"/"]; + NSString* dest = split[0]; + + if ([dest containsString:@":"]) { + // IPv6 address + struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&route->m_storage; + memset(sin6, 0, sizeof(struct sockaddr_in6)); + sin6->sin6_family = AF_INET6; + sin6->sin6_len = sizeof(struct sockaddr_in6); + sin6->sin6_port = 0; // Not used. + if (!inet_pton(AF_INET6, dest.UTF8String, &sin6->sin6_addr.s6_addr)) { + return nil; + } + route->_prefixLength = 128; + } else { + struct sockaddr_in* sin = (struct sockaddr_in*)&route->m_storage; + memset(sin, 0, sizeof(struct sockaddr_in)); + sin->sin_family = AF_INET; + sin->sin_len = sizeof(struct sockaddr_in); + sin->sin_port = 0; // Not used. + if (!inet_pton(AF_INET, dest.UTF8String, &sin->sin_addr.s_addr)) { + return nil; + } + route->_prefixLength = 32; + } + + if (split.count > 1) { + NSInteger i = split[1].integerValue; + if ((i < 0) || (i > route.prefixLength)) { + return nil; + } + route->_prefixLength = i; + } + + return route; +} + +- (const struct sockaddr*)getDestination { + return (const struct sockaddr*)&m_storage; +} + @end @implementation InterfaceConfig @@ -119,43 +165,10 @@ + (id)parseFromDict:(NSDictionary *)dict { if ([rangesObject isKindOfClass:[NSArray class]]) { NSArray* list = (NSArray*)rangesObject; for (NSString* rangeString in list) { - NSArray* split = [rangeString componentsSeparatedByString:@"/"]; - NSString* dest = split[0]; - RoutePrefix* prefix = [RoutePrefix new]; - - if ([dest containsString:@":"]) { - // IPv6 address - struct sockaddr_in6 sin6; - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(sin6); - sin6.sin6_port = 0; // Not used. - if (!inet_pton(AF_INET6, dest.UTF8String, &sin6.sin6_addr.s6_addr)) { - return nil; - } - prefix.destination = nw_endpoint_create_address((struct sockaddr*)&sin6); - prefix.prefixLength = 128; - } else { - struct sockaddr_in sin; - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_len = sizeof(sin); - sin.sin_port = 0; // Not used. - if (!inet_pton(AF_INET, dest.UTF8String, &sin.sin_addr.s_addr)) { - return nil; - } - prefix.destination = nw_endpoint_create_address((struct sockaddr*)&sin); - prefix.prefixLength = 32; + RoutePrefix* prefix = [RoutePrefix parseRoute:rangeString]; + if (!prefix) { + return nil; } - - if (split.count > 1) { - NSInteger i = split[1].integerValue; - if ((i < 0) || (i > prefix.prefixLength)) { - return nil; - } - prefix.prefixLength = i; - } - [routes addObject:prefix]; } } diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 5dfccff968..d23a75cd0e 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -460,7 +460,6 @@ - (void) handleWireguard:(int)op {.iov_base = (void *)packet.bytes, .iov_len = packet.length}, }; int err = writev(m_tunfd, iov, 2); - NSLog(@"utun write: %d", err); break; } } From 1648a0f87f03b42dca11dbcd0714b393302a686c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 10:49:48 -0700 Subject: [PATCH 10/31] Improve the debug experience by installing dSYM into bundle --- macos/networkextension/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index 10fc76a310..e67f76d8ad 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -52,6 +52,15 @@ set_target_properties(networkextension PROPERTIES target_compile_options(networkextension PUBLIC "-fobjc-arc" "-fno-objc-arc-exceptions") target_compile_definitions(networkextension PRIVATE "$<$:MZ_DEBUG>") +# Improve the macOS debug experience - tune for LLDB and copy dSYM into +# the bundle when we are building for the Debug configuration. +target_compile_options(networkextension PRIVATE $<$:-O0 -gfull -glldb>) +target_link_options(networkextension PRIVATE $<$:-O0 -gfull -glldb>) +add_custom_command(TARGET networkextension POST_BUILD + COMMAND ${CMAKE_COMMAND} -E rm -rf $.dSYM + COMMAND ${CMAKE_COMMAND} -E $,env,true> -- dsymutil $ -o $.dSYM +) + target_link_libraries(networkextension PRIVATE ${FW_FOUNDATION}) target_link_libraries(networkextension PRIVATE ${FW_NW_EXTENSION}) target_sources(networkextension PRIVATE From 8eb278de51ccb7c4b66ba45174d07a387f8f3e26 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 11:11:39 -0700 Subject: [PATCH 11/31] Fix double-release in socket/source cleanup --- macos/networkextension/routemanager.mm | 18 +++---- macos/networkextension/wireguardtunnel.mm | 63 +++++++---------------- 2 files changed, 26 insertions(+), 55 deletions(-) diff --git a/macos/networkextension/routemanager.mm b/macos/networkextension/routemanager.mm index 0ecc8a72b9..4ac7f67612 100644 --- a/macos/networkextension/routemanager.mm +++ b/macos/networkextension/routemanager.mm @@ -21,9 +21,7 @@ @implementation RouteManager { // The routing socket. - CFSocketNativeHandle m_sockfd; CFSocketRef m_socket; - CFRunLoopSourceRef m_source; int m_rtseq; NSObject* m_delegate; @@ -188,17 +186,17 @@ - (id)init { NSLog(@"route manager created"); m_rtseq = 0; - m_sockfd = socket(PF_ROUTE, SOCK_RAW, 0); - if (m_sockfd < 0) { + int sockfd = socket(PF_ROUTE, SOCK_RAW, 0); + if (sockfd < 0) { NSLog(@"failed to create routing socket: %s", strerror(errno)); } CFSocketContext ctx = { .info = (__bridge void *)self }; - m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, m_sockfd, kCFSocketDataCallBack, + m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, sockfd, kCFSocketDataCallBack, rawSockCallback, &ctx); // Create a source and attach it to the main run loop. - m_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); return self; } @@ -208,10 +206,6 @@ - (void)dealloc { CFSocketInvalidate(m_socket); CFRelease(m_socket); - close(m_sockfd); - - CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); - CFRelease(m_source); #if !__has_feature(objc_arc) [super dealloc]; @@ -501,7 +495,7 @@ - (void) rtmSendRoute:(int)action } // Send the routing message into the kernel. - int len = write(m_sockfd, rtm, rtm->rtm_msglen); + int len = write(CFSocketGetNative(m_socket), rtm, rtm->rtm_msglen); if (len == rtm->rtm_msglen) { return; } diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index d23a75cd0e..9c88985565 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -38,9 +38,7 @@ @implementation WireguardStats @implementation WireguardTunnel { struct wireguard_tunnel* m_wireguard; - int m_tunfd; CFSocketRef m_socket; - CFRunLoopSourceRef m_source; CFRunLoopTimerRef m_timer; struct timespec m_lastHandshake; @@ -83,7 +81,6 @@ - (id)init { self = [super init]; set_logging_function(wgLog); - m_tunfd = -1; m_wireguard = nil; _mtu = IPV6_MMTU; @@ -105,15 +102,22 @@ - (id)init { - (void) startTunnelWithOptions:(InterfaceConfig *)options completionHandler:(void (^)(NSError *error)) completionHandler { m_completionHandler = completionHandler; - m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); - if (m_tunfd < 0) { + + int tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); + if (tunfd < 0) { [self shutdownTunnel:@"tunnel creation failed" withErrno:errno]; return; } + // Wrap the tunnel device in a CFSocket + CFSocketContext ctx = { .info = (__bridge void *)self }; + m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, tunfd, + kCFSocketDataCallBack, + utunSockCallback, &ctx); + // Connect to the utun control kernel service. struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; - int err = ioctl(m_tunfd, CTLIOCGINFO, &info); + int err = ioctl(tunfd, CTLIOCGINFO, &info); if (err < 0) { [self shutdownTunnel:@"kernel utun lookup failed" withErrno:errno]; return; @@ -124,7 +128,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options addr.ss_sysaddr = AF_SYS_CONTROL; addr.sc_id = info.ctl_id; addr.sc_unit = 0; - err = connect(m_tunfd, (struct sockaddr*)&addr, sizeof(addr)); + err = connect(tunfd, (struct sockaddr*)&addr, sizeof(addr)); if (err < 0) { [self shutdownTunnel:@"kernel utun connect failed" withErrno:errno]; return; @@ -133,7 +137,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Get the tunnel device's name. struct ifreq ifr; socklen_t ifnamesize = sizeof(ifr.ifr_name); - err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, + err = getsockopt(tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, &ifnamesize); if (err < 0) { [self shutdownTunnel:@"utun name loookup failed" withErrno:errno]; @@ -154,33 +158,27 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Set a base MTU, it will get updated later. ifr.ifr_mtu = self.mtu; - if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { + if (ioctl(tunfd, SIOCSIFMTU, &ifr) != 0) { [self shutdownTunnel:@"failed to set mtu" withErrno:errno]; return; } // Bring the device up. - err = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); + err = ioctl(tunfd, SIOCGIFFLAGS, &ifr); if (err != 0) { [self shutdownTunnel:@"failed to get interface flags" withErrno:errno]; return; } ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); - err = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); + err = ioctl(tunfd, SIOCSIFFLAGS, &ifr); if (err != 0) { [self shutdownTunnel:@"failed to set device up" withErrno:errno]; return; } - // Wrap the tunnel device in a CFSocket - CFSocketContext ctx = { .info = (__bridge void *)self }; - m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, m_tunfd, - kCFSocketDataCallBack, - utunSockCallback, &ctx); - // Create a source and attach it to the main run loop. - m_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); // (Re)-create the Wireguard tunnel structure. if (m_wireguard) { @@ -459,7 +457,7 @@ - (void) handleWireguard:(int)op {.iov_base = &header, .iov_len = sizeof(header)}, {.iov_base = (void *)packet.bytes, .iov_len = packet.length}, }; - int err = writev(m_tunfd, iov, 2); + int err = writev(CFSocketGetNative(m_socket), iov, 2); break; } } @@ -492,17 +490,6 @@ - (void)shutdownTunnel:(NSError*)error { m_socket = nil; } - if (m_source) { - CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); - CFRelease(m_source); - m_source = nil; - } - - if (m_tunfd >= 0) { - close(m_tunfd); - m_tunfd = -1; - } - if (m_wireguard) { tunnel_free(m_wireguard); m_wireguard = nil; @@ -543,21 +530,11 @@ - (WireguardStats*)getStatus { - (void)setMtu:(NSUInteger)mtu { _mtu = mtu; - if (m_tunfd < 0) { - return; - } struct ifreq ifr; - socklen_t ifnamesize = sizeof(ifr.ifr_name); - int err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, - &ifnamesize); - if (err < 0) { - NSLog(@"mtu failed to find ifname: %s", strerror(errno)); - return; - } - + strncpy(ifr.ifr_name, nw_interface_get_name(self.virtualInterface), sizeof(ifr.ifr_name)); ifr.ifr_mtu = _mtu; - if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { + if (ioctl(CFSocketGetNative(m_socket), SIOCSIFMTU, &ifr) != 0) { NSLog(@"mtu update failed: %s", strerror(errno)); } } From 4b6f03309ef34956c5a9166ca763f70275c01acc Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 15:17:36 -0700 Subject: [PATCH 12/31] Optimize heap usage and chase down a crash during tunnel shutdown --- macos/networkextension/wireguardtunnel.mm | 203 ++++++++++++---------- 1 file changed, 114 insertions(+), 89 deletions(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 9c88985565..5f9cc2482d 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -61,15 +61,11 @@ static void utunSockCallback(CFSocketRef s, CFSocketCallBackType cbType, CFDataRef address, const void * data, void *info) { WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; NSData* rawData = (__bridge NSData*)data; - if (cbType != kCFSocketDataCallBack) { + if (cbType != kCFSocketReadCallBack) { NSLog(@"utunSockCallback: unexpected type %d", (int)cbType); - } else if (rawData.length < 4) { - NSLog(@"utunSockCallback: packet truncated"); - } else { - NSData* packet = [rawData subdataWithRange:NSMakeRange(4, rawData.length - 4)]; - [tunnel handleOutbound:packet - withProtocol:htonl(*(uint32_t*)rawData.bytes)]; + return; } + [tunnel handleOutbound]; } static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { @@ -112,7 +108,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Wrap the tunnel device in a CFSocket CFSocketContext ctx = { .info = (__bridge void *)self }; m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, tunfd, - kCFSocketDataCallBack, + kCFSocketReadCallBack, utunSockCallback, &ctx); // Connect to the utun control kernel service. @@ -176,25 +172,23 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options return; } - // Create a source and attach it to the main run loop. - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); - - // (Re)-create the Wireguard tunnel structure. - if (m_wireguard) { - tunnel_free(m_wireguard); - } - uint32_t index; - getentropy(&index, sizeof(index)); - +#ifdef MZ_DEBUG char *addrstr = nw_endpoint_copy_address_string(options.serverIpv4Addr); NSLog(@"wireguard peer: %s port=%d", addrstr, nw_endpoint_get_port(options.serverIpv4Addr)); free(addrstr); +#endif nw_parameters_t params = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); self.connection = nw_connection_create(options.serverIpv4Addr, params); nw_connection_set_queue(self.connection, dispatch_get_main_queue()); + // (Re)-create the Wireguard tunnel structure. + if (m_wireguard) { + tunnel_free(m_wireguard); + } + + uint32_t index; + getentropy(&index, sizeof(index)); m_wireguard = new_tunnel(options.privateKey.UTF8String, options.serverPublicKey.UTF8String, nil, // Preshared key @@ -207,11 +201,10 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options if (err) { CFErrorRef cfError = nw_error_copy_cf_error(err); NSLog(@"vpn socket error: %@", (__bridge NSError*)cfError); - [self shutdownTunnel:(__bridge NSError*)cfError]; + [self cancelTunnelWithError:(__bridge NSError*)cfError]; CFRelease(cfError); } else if (state == nw_connection_state_cancelled || state == nw_connection_state_failed) { NSLog(@"vpn socket closed"); - [self shutdownTunnel:nil]; } else if (state != nw_connection_state_ready) { NSLog(@"vpn socket state %d", state); } else { @@ -227,6 +220,10 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options } }); nw_connection_start(self.connection); + + // Start processing tunnel socket events. + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); } - (void) stopTunnelWithReason:(NEProviderStopReason)reason @@ -300,19 +297,18 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { - (void) renegotiate { NSLog(@"wireguard renegotiate"); - UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); + uint8_t* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); - struct wireguard_result r; - r = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + struct wireguard_result result; + result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); // Set a timeout for the handshake to finish. clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); m_handshakeTimeout.tv_sec += WG_MAX_HANDSHAKE_TIMEOUT; - dispatch_data_t data = dispatch_data_create(handshake, r.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_FREE); - [self handleWireguard:(int)r.op withData:data]; + NSData* data = [NSData dataWithBytesNoCopy:handshake + length:WG_MAX_HANDSHAKE_SIZE]; + [self handleWireguard:result withData:data]; } - (void) handleTimer { @@ -331,43 +327,67 @@ - (void) handleTimer { } } - UInt8* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); - struct wireguard_result r; - r = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + uint8_t* handshake = (uint8_t *)malloc(WG_MAX_HANDSHAKE_SIZE); + struct wireguard_result result; + result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); - dispatch_data_t data = dispatch_data_create(handshake, r.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_FREE); - [self handleWireguard:(int)r.op withData:data]; + NSData* data = [NSData dataWithBytesNoCopy:handshake + length:WG_MAX_HANDSHAKE_SIZE]; + [self handleWireguard:result withData:data]; } -- (void) handleOutbound:(NSData*)packet - withProtocol:(int)protocol { - if (!m_wireguard) { - return; - } +- (void) handleOutbound { + size_t mtu = self.mtu; + uint8_t buffer[mtu + 16]; + uint32_t header; + + struct iovec iov[2]; + iov[0].iov_base = &header; + iov[0].iov_len = sizeof(header); + iov[1].iov_base = buffer; + iov[1].iov_len = mtu; + + struct msghdr msg = { + .msg_name = nullptr, + .msg_namelen = 0, + .msg_iov = iov, + .msg_iovlen = 2, + .msg_control = nullptr, + .msg_controllen = 0, + .msg_flags = 0 + }; - // I think there is a small bug in boringtun to do with message padding. - // The wireguard protocol states that the encapsulated packet must first - // be padded out to a multiple of 16 bytes in length, but boringtun does - // no such padding during encryption. So let's do it manually ourself. - NSMutableData* plaintext = [NSMutableData dataWithData:packet]; - int tail = plaintext.length % 16; - if (tail) { - [plaintext increaseLengthBy:16 - tail]; - } + while (true) { + int rx = recvmsg(CFSocketGetNative(m_socket), &msg, MSG_DONTWAIT); + if (rx < 0) { + // Socket error occurred. + if (errno == EINTR) continue; + return; + } + int pktlen = rx - sizeof(header); + if ((pktlen < 0) || (pktlen > mtu)) { + continue; + } + + // I think there is a small bug in boringtun to do with message padding. + // The wireguard protocol states that the encapsulated packet must first + // be padded out to a multiple of 16 bytes in length, but boringtun does + // no such padding during encryption. So let's do it manually ourself. + int tail = pktlen % 16; + if (tail) { + memset(buffer + pktlen, 0, 16 - tail); + pktlen += 16 - tail; + } + + // Encrypt the wireguard packet. + uint8_t *ciphertext = (uint8_t *)malloc(mtu + WG_MTU_OVERHEAD); + struct wireguard_result result; + result = wireguard_write(m_wireguard, buffer, pktlen, ciphertext, mtu + WG_MTU_OVERHEAD); - // Encrypt the packet. - size_t length = plaintext.length + WG_PACKET_OVERHEAD; - uint8_t* ciphertext = (uint8_t*)malloc(length); - struct wireguard_result r; - r = wireguard_write(m_wireguard, (const uint8_t*)plaintext.bytes, - plaintext.length, ciphertext, length); - - dispatch_data_t data = dispatch_data_create(ciphertext, r.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_FREE); - [self handleWireguard:(int)r.op withData:data]; + NSData* data = [NSData dataWithBytesNoCopy:ciphertext + length:mtu + WG_MTU_OVERHEAD]; + [self handleWireguard:result withData:data]; + } } - (void) handleInbound { @@ -392,9 +412,9 @@ - (void) handleInbound { uint8_t* plaintext = (uint8_t*)malloc(length); // Decrypt the wireguard packet. - struct wireguard_result r; - r = wireguard_read(m_wireguard, (const uint8_t*)ciphertext, length, - plaintext, length); + struct wireguard_result result; + result = wireguard_read(m_wireguard, (const uint8_t*)ciphertext, length, + plaintext, length); // After processing a handshake response, update the lastHandshake time // if it looks and smells like the handshake was successful. @@ -416,48 +436,53 @@ - (void) handleInbound { } } - dispatch_data_t packet = dispatch_data_create(plaintext, r.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_FREE); - [self handleWireguard:(int)r.op withData:packet]; + NSData* packet = [NSData dataWithBytesNoCopy:plaintext length:length]; + [self handleWireguard:result withData:packet]; // Keep going to receive more data. [self handleInbound]; }); } -- (void) handleWireguard:(int)op - withData:(dispatch_data_t)data { - switch (op) { +- (void) handleWireguard:(struct wireguard_result)result + withData:(NSData*)data { + uint32_t header = htonl(AF_INET); + switch (result.op) { case WIREGUARD_DONE: break; case WRITE_TO_NETWORK: - nw_connection_send(self.connection, data, - NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, - ^(nw_error_t error) { - if (error) { - CFErrorRef cfError = nw_error_copy_cf_error(error); - NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); - CFRelease(cfError); - } - }); + if (result.size <= data.length) { + dispatch_data_t dgram = dispatch_data_create(data.bytes, result.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_DEFAULT); + nw_connection_send(self.connection, dgram, + NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, + ^(nw_error_t error) { + if (error) { + CFErrorRef cfError = nw_error_copy_cf_error(error); + NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); + CFRelease(cfError); + } + }); + } break; case WIREGUARD_ERROR: - NSLog(@"wireguard error"); + NSLog(@"wireguard error: %zu", result.size); break; - case WRITE_TO_TUNNEL_IPV4: - [[fallthrough]]; case WRITE_TO_TUNNEL_IPV6: - NSData* packet = (NSData*)data; - uint32_t header = (op == WRITE_TO_TUNNEL_IPV6) ? htonl(AF_INET6) : htonl(AF_INET); - const struct iovec iov[2] = { - {.iov_base = &header, .iov_len = sizeof(header)}, - {.iov_base = (void *)packet.bytes, .iov_len = packet.length}, - }; - int err = writev(CFSocketGetNative(m_socket), iov, 2); + header = htonl(AF_INET6); + [[fallthrough]]; + case WRITE_TO_TUNNEL_IPV4: + if (result.size <= data.length) { + const struct iovec iov[2] = { + {.iov_base = &header, .iov_len = sizeof(header)}, + {.iov_base = (void *)data.bytes, .iov_len = result.size}, + }; + int err = writev(CFSocketGetNative(m_socket), iov, 2); + } break; } } From ecb85ef8adf86fa12c397d52fabe68b53215b65c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 15:19:29 -0700 Subject: [PATCH 13/31] Invert split tunneling logic - included flows are bound into VPN --- .../VPNSplitTunnelProvider.mm | 147 ++---------------- 1 file changed, 12 insertions(+), 135 deletions(-) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index 9043ea130f..4302facc5a 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -40,10 +40,6 @@ - (void)defaultRouteChanged:(int)family @property (strong) WireguardTunnel* wireguard; @property (strong) InterfaceConfig* config; -@property (strong) nw_interface_t ipv4Interface; -@property (strong) nw_interface_t ipv6Interface; -@property (strong) nw_interface_t vpnInterface; - @property (strong) NSMutableArray* vpnDisabledApps; @end @@ -284,41 +280,6 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler { NSLog(@"stopping proxy"); - // Remove captured default routes, if known. - if (self.ipv4Interface) { - NSLog(@"clearing cloned ipv4 route"); - - struct sockaddr_in sin; - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_len = sizeof(sin); - - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&sin - withPrefix:0 - viaInterface:self.ipv4Interface - withGateway:nil - andFlags:RTF_IFSCOPE]; - - self.ipv4Interface = nil; - } - if (self.ipv6Interface) { - NSLog(@"clearing cloned ipv6 route"); - - struct sockaddr_in6 sin6; - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(sin6); - - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&sin6 - withPrefix:0 - viaInterface:self.ipv6Interface - withGateway:nil - andFlags:RTF_IFSCOPE]; - - self.ipv6Interface = nil; - } self.routeManager = nil; NSLog(@"handled tcp flows: %lld", std::atomic_load(&m_handledTcpFlows)); @@ -330,17 +291,17 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason } - (BOOL)matchAppFlow:(NEAppProxyFlow*)flow { - // Without metadata - do not exclude the application. + // Without metadata - always direct the flow into the VPN. if (flow.metaData == nil) { - return NO; + return YES; } - // If not signed - do not exclude the application. + // If not signed - always direct the flow into the VPN. if (flow.metaData.sourceAppSigningIdentifier == nil) { #ifdef MZ_DEBUG NSLog(@"new flow: unsigned -> %@", flow.remoteHostname); #endif - return NO; + return YES; } NSString* sourceId = flow.metaData.sourceAppSigningIdentifier; @@ -364,16 +325,16 @@ - (BOOL)matchAppFlow:(NEAppProxyFlow*)flow { options:NSLiteralSearch range:NSMakeRange(0, appId.length)]; if (result == NSOrderedSame) { - return YES; + return NO; } } - // No application matches this signing identifier. - return NO; + // No application matches this signing identifier - direct the flow into the VPN. + return YES; } - (BOOL)handleNewFlow:(NEAppProxyFlow*) flow { - // Evaluate whether the source of this flow should be excluded from the VPN. + // Evaluate whether the source of this flow should be redirected into the VPN. if (![self matchAppFlow:flow]) { return NO; } @@ -381,21 +342,16 @@ - (BOOL)handleNewFlow:(NEAppProxyFlow*) flow { // Perform flow bypassing. if ([flow isKindOfClass:[NEAppProxyTCPFlow class]]) { NEAppProxyTCPFlow* tcpFlow = (NEAppProxyTCPFlow*)flow; - nw_interface_t via = self.ipv4Interface; nw_endpoint_t dest = nil; if (@available(macOS 15, *)) { dest = tcpFlow.remoteFlowEndpoint; } else { dest = [VPNSplitTunnelProvider convertEndpoint:tcpFlow.remoteEndpoint]; } - if (nw_endpoint_get_type(dest) == nw_endpoint_type_address && - nw_endpoint_get_address(dest)->sa_family == AF_INET6) { - via = self.ipv6Interface; - } BypassTcpFlow* handler = [BypassTcpFlow createBypass:tcpFlow toEndpoint:dest - withInterface:via]; + withInterface:self.wireguard.virtualInterface]; if (!handler) { return NO; } @@ -410,21 +366,16 @@ - (BOOL)handleNewFlow:(NEAppProxyFlow*) flow { return YES; } else if ([flow isKindOfClass:[NEAppProxyUDPFlow class]]) { NEAppProxyUDPFlow* udpFlow = (NEAppProxyUDPFlow*)flow; - nw_interface_t via = self.ipv4Interface; nw_endpoint_t source; if (@available(macOS 15, *)) { source = udpFlow.localFlowEndpoint; } else { source = [VPNSplitTunnelProvider convertEndpoint:udpFlow.localEndpoint]; } - if (source && (nw_endpoint_get_type(source) == nw_endpoint_type_address) && - (nw_endpoint_get_address(source)->sa_family == AF_INET6)) { - via = self.ipv6Interface; - } BypassUdpFlow* handler = [BypassUdpFlow createBypass:udpFlow localEndpoint:source - withInterface:via]; + withInterface:self.wireguard.virtualInterface]; if (!handler) { return NO; } @@ -535,91 +486,17 @@ - (void)defaultRouteChanged:(int)family int ifindex = interface ? nw_interface_get_index(interface) : 0; if (family == AF_INET) { - struct sockaddr_in dst; - memset(&dst, 0, sizeof(dst)); - dst.sin_family = AF_INET; - dst.sin_len = sizeof(dst); - if (interface) { - int action = RTM_ADD; NSLog(@"default ipv4 route via %s", nw_interface_get_name(interface)); - - if (!self.ipv4Interface) { - action = RTM_ADD; - } else if (ifindex == nw_interface_get_index(self.ipv4Interface)) { - action = RTM_CHANGE; - } else { - // Default route has changed interface - delete the old clone. - action = RTM_ADD; - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:self.ipv4Interface - withGateway:nil - andFlags:RTF_IFSCOPE]; - } - - // Add or update the cloned default route. - [self.routeManager rtmSendRoute:action - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:interface - withGateway:gateway - andFlags:RTF_IFSCOPE]; - } else if (self.ipv4Interface) { + } else { NSLog(@"default ipv4 route lost"); - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:self.ipv4Interface - withGateway:gateway - andFlags:RTF_IFSCOPE]; } - - self.ipv4Interface = interface; } else if (family == AF_INET6) { - struct sockaddr_in6 dst; - memset(&dst, 0, sizeof(dst)); - dst.sin6_family = AF_INET6; - dst.sin6_len = sizeof(dst); - if (interface) { - int action = RTM_ADD; NSLog(@"default ipv6 route via %s", nw_interface_get_name(interface)); - - if (!self.ipv6Interface) { - action = RTM_ADD; - } else if (ifindex == nw_interface_get_index(self.ipv6Interface)) { - action = RTM_CHANGE; - } else { - action = RTM_ADD; - // Default route has changed interface - delete the old clone. - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:self.ipv6Interface - withGateway:nil - andFlags:RTF_IFSCOPE]; - } - - // Add or update the cloned default route. - [self.routeManager rtmSendRoute:action - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:interface - withGateway:gateway - andFlags:RTF_IFSCOPE]; - } else if (self.ipv6Interface) { + } else { NSLog(@"default ipv6 route lost"); - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:(struct sockaddr*)&dst - withPrefix:0 - viaInterface:interface - withGateway:gateway - andFlags:RTF_IFSCOPE]; } - - self.ipv6Interface = interface; } } From b7152ebaf62f931b415150e70fa9e0c14ae34419 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 15:32:48 -0700 Subject: [PATCH 14/31] Calculate MTU dynamically --- macos/networkextension/wireguardtunnel.mm | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 5f9cc2482d..e8006c30f5 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -49,7 +49,6 @@ @implementation WireguardTunnel { } constexpr const int WG_PACKET_OVERHEAD = 32; -constexpr const int WG_MTU_OVERHEAD = 80; constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; @@ -209,14 +208,18 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options NSLog(@"vpn socket state %d", state); } else { NSLog(@"vpn socket opened"); - [self renegotiate]; - [self handleInbound]; + // Update the MTU once the socket is open + self.mtu = nw_connection_get_maximum_datagram_size(self.connection) - WG_PACKET_OVERHEAD; // Start the timer. CFRunLoopTimerContext timerContext = { .info = (__bridge void *)self }; m_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 0.1, 0, 0, wgTimerCallback, &timerContext); CFRunLoopAddTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + + // Force an initial handshake and begin packet processing. + [self renegotiate]; + [self handleInbound]; } }); nw_connection_start(self.connection); @@ -380,12 +383,13 @@ - (void) handleOutbound { } // Encrypt the wireguard packet. - uint8_t *ciphertext = (uint8_t *)malloc(mtu + WG_MTU_OVERHEAD); + size_t enclength = mtu + WG_PACKET_OVERHEAD; + uint8_t *encrypt = (uint8_t *)malloc(enclength); struct wireguard_result result; - result = wireguard_write(m_wireguard, buffer, pktlen, ciphertext, mtu + WG_MTU_OVERHEAD); + result = wireguard_write(m_wireguard, buffer, pktlen, encrypt, enclength); - NSData* data = [NSData dataWithBytesNoCopy:ciphertext - length:mtu + WG_MTU_OVERHEAD]; + NSData* data = [NSData dataWithBytesNoCopy:encrypt + length:enclength]; [self handleWireguard:result withData:data]; } } From a04d0d74e47629fb0695aaa087060a2d579bb60b Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 23 Apr 2026 18:16:50 -0700 Subject: [PATCH 15/31] Use a dispatch queue to mutlithread the encryption workers --- macos/networkextension/wireguardtunnel.mm | 181 ++++++++++++---------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index e8006c30f5..115cabb2cd 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -38,8 +39,9 @@ @implementation WireguardStats @implementation WireguardTunnel { struct wireguard_tunnel* m_wireguard; - CFSocketRef m_socket; + int m_tunfd; CFRunLoopTimerRef m_timer; + dispatch_group_t m_workqueue; struct timespec m_lastHandshake; struct timespec m_handshakeTimeout; @@ -51,22 +53,12 @@ @implementation WireguardTunnel { constexpr const int WG_PACKET_OVERHEAD = 32; constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; +constexpr const int64_t WG_WORKQUEUE_TIMEOUT = 30; static void wgLog(const char* msg) { NSLog(@"wg: %s", msg); } -static void utunSockCallback(CFSocketRef s, CFSocketCallBackType cbType, - CFDataRef address, const void * data, void *info) { - WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; - NSData* rawData = (__bridge NSData*)data; - if (cbType != kCFSocketReadCallBack) { - NSLog(@"utunSockCallback: unexpected type %d", (int)cbType); - return; - } - [tunnel handleOutbound]; -} - static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; [tunnel handleTimer]; @@ -76,6 +68,7 @@ - (id)init { self = [super init]; set_logging_function(wgLog); + m_tunfd = -1; m_wireguard = nil; _mtu = IPV6_MMTU; @@ -94,25 +87,33 @@ - (id)init { userInfo:@{NSLocalizedDescriptionKey: msg}]; } +// Aim to allocate roughly half the total core count as workers. +static int getWorkerCount() { + int mib[] = { CTL_HW, HW_NCPU }; + int count = 4; + size_t length = sizeof(count); + sysctl(mib, sizeof(mib)/sizeof(int), &count, &length, nullptr, 0); + if (count < 2) { + return 1; + } else { + return count / 2; + } +} + - (void) startTunnelWithOptions:(InterfaceConfig *)options completionHandler:(void (^)(NSError *error)) completionHandler { m_completionHandler = completionHandler; + m_workqueue = dispatch_group_create(); - int tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); - if (tunfd < 0) { + m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); + if (m_tunfd < 0) { [self shutdownTunnel:@"tunnel creation failed" withErrno:errno]; return; } - // Wrap the tunnel device in a CFSocket - CFSocketContext ctx = { .info = (__bridge void *)self }; - m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, tunfd, - kCFSocketReadCallBack, - utunSockCallback, &ctx); - // Connect to the utun control kernel service. struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; - int err = ioctl(tunfd, CTLIOCGINFO, &info); + int err = ioctl(m_tunfd, CTLIOCGINFO, &info); if (err < 0) { [self shutdownTunnel:@"kernel utun lookup failed" withErrno:errno]; return; @@ -123,7 +124,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options addr.ss_sysaddr = AF_SYS_CONTROL; addr.sc_id = info.ctl_id; addr.sc_unit = 0; - err = connect(tunfd, (struct sockaddr*)&addr, sizeof(addr)); + err = connect(m_tunfd, (struct sockaddr*)&addr, sizeof(addr)); if (err < 0) { [self shutdownTunnel:@"kernel utun connect failed" withErrno:errno]; return; @@ -132,7 +133,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Get the tunnel device's name. struct ifreq ifr; socklen_t ifnamesize = sizeof(ifr.ifr_name); - err = getsockopt(tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, + err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, &ifnamesize); if (err < 0) { [self shutdownTunnel:@"utun name loookup failed" withErrno:errno]; @@ -153,19 +154,19 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Set a base MTU, it will get updated later. ifr.ifr_mtu = self.mtu; - if (ioctl(tunfd, SIOCSIFMTU, &ifr) != 0) { + if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { [self shutdownTunnel:@"failed to set mtu" withErrno:errno]; return; } // Bring the device up. - err = ioctl(tunfd, SIOCGIFFLAGS, &ifr); + err = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); if (err != 0) { [self shutdownTunnel:@"failed to get interface flags" withErrno:errno]; return; } ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); - err = ioctl(tunfd, SIOCSIFFLAGS, &ifr); + err = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); if (err != 0) { [self shutdownTunnel:@"failed to set device up" withErrno:errno]; return; @@ -220,13 +221,14 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Force an initial handshake and begin packet processing. [self renegotiate]; [self handleInbound]; + + // Start outbound encryption workers. + for (int i = 0; i < getWorkerCount(); i++) { + [self startOutboundWorker]; + } } }); nw_connection_start(self.connection); - - // Start processing tunnel socket events. - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); } - (void) stopTunnelWithReason:(NEProviderStopReason)reason @@ -339,59 +341,59 @@ - (void) handleTimer { [self handleWireguard:result withData:data]; } -- (void) handleOutbound { - size_t mtu = self.mtu; - uint8_t buffer[mtu + 16]; - uint32_t header; - - struct iovec iov[2]; - iov[0].iov_base = &header; - iov[0].iov_len = sizeof(header); - iov[1].iov_base = buffer; - iov[1].iov_len = mtu; - - struct msghdr msg = { - .msg_name = nullptr, - .msg_namelen = 0, - .msg_iov = iov, - .msg_iovlen = 2, - .msg_control = nullptr, - .msg_controllen = 0, - .msg_flags = 0 - }; - - while (true) { - int rx = recvmsg(CFSocketGetNative(m_socket), &msg, MSG_DONTWAIT); - if (rx < 0) { - // Socket error occurred. - if (errno == EINTR) continue; - return; - } - int pktlen = rx - sizeof(header); - if ((pktlen < 0) || (pktlen > mtu)) { - continue; - } +- (void) startOutboundWorker { + dispatch_group_async(m_workqueue, + dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), + ^(void){ + size_t mtu = self.mtu; + uint8_t buffer[mtu + 16]; + uint32_t header; + + struct iovec iov[2]; + iov[0].iov_base = &header; + iov[0].iov_len = sizeof(header); + iov[1].iov_base = buffer; + iov[1].iov_len = mtu; + + while (true) { + int rx = readv(m_tunfd, iov, 2); + if (rx == 0) { + // Socket has closed. + NSLog(@"utun closed"); + return; + } + if (rx < 0) { + // Socket error occurred. + NSLog(@"utun error: %s", strerror(errno)); + if (errno == EINTR) continue; + return; + } + int pktlen = rx - sizeof(header); + if ((pktlen < 0) || (pktlen > mtu)) { + continue; + } - // I think there is a small bug in boringtun to do with message padding. - // The wireguard protocol states that the encapsulated packet must first - // be padded out to a multiple of 16 bytes in length, but boringtun does - // no such padding during encryption. So let's do it manually ourself. - int tail = pktlen % 16; - if (tail) { - memset(buffer + pktlen, 0, 16 - tail); - pktlen += 16 - tail; - } + // I think there is a small bug in boringtun to do with message padding. + // The wireguard protocol states that the encapsulated packet must first + // be padded out to a multiple of 16 bytes in length, but boringtun does + // no such padding during encryption. So let's do it manually ourself. + int tail = pktlen % 16; + if (tail) { + memset(buffer + pktlen, 0, 16 - tail); + pktlen += 16 - tail; + } - // Encrypt the wireguard packet. - size_t enclength = mtu + WG_PACKET_OVERHEAD; - uint8_t *encrypt = (uint8_t *)malloc(enclength); - struct wireguard_result result; - result = wireguard_write(m_wireguard, buffer, pktlen, encrypt, enclength); + // Encrypt the wireguard packet. + size_t enclength = mtu + WG_PACKET_OVERHEAD; + uint8_t *encrypt = (uint8_t *)malloc(enclength); + struct wireguard_result result; + result = wireguard_write(m_wireguard, buffer, pktlen, encrypt, enclength); - NSData* data = [NSData dataWithBytesNoCopy:encrypt - length:enclength]; - [self handleWireguard:result withData:data]; - } + NSData* data = [NSData dataWithBytesNoCopy:encrypt + length:enclength]; + [self handleWireguard:result withData:data]; + } + }); } - (void) handleInbound { @@ -485,7 +487,7 @@ - (void) handleWireguard:(struct wireguard_result)result {.iov_base = &header, .iov_len = sizeof(header)}, {.iov_base = (void *)data.bytes, .iov_len = result.size}, }; - int err = writev(CFSocketGetNative(m_socket), iov, 2); + int err = writev(m_tunfd, iov, 2); } break; } @@ -513,10 +515,17 @@ - (void)shutdownTunnel:(NSError*)error { m_timer = nil; } - if (m_socket) { - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - m_socket = nil; + // Shutdown the tunnel workers. + if (m_workqueue) { + shutdown(m_tunfd, SHUT_RDWR); + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, WG_WORKQUEUE_TIMEOUT * 1000000000); + dispatch_group_wait(m_workqueue, delay); + m_workqueue = nil; + } + + if (m_tunfd >= 0) { + close(m_tunfd); + m_tunfd = -1; } if (m_wireguard) { @@ -563,7 +572,7 @@ - (void)setMtu:(NSUInteger)mtu { struct ifreq ifr; strncpy(ifr.ifr_name, nw_interface_get_name(self.virtualInterface), sizeof(ifr.ifr_name)); ifr.ifr_mtu = _mtu; - if (ioctl(CFSocketGetNative(m_socket), SIOCSIFMTU, &ifr) != 0) { + if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { NSLog(@"mtu update failed: %s", strerror(errno)); } } From af688b964b99435a05b2d44da58619ec10030d9d Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 24 Apr 2026 00:01:16 -0700 Subject: [PATCH 16/31] More memory refinement - I think I fixed a leak too --- macos/networkextension/wireguardtunnel.mm | 87 ++++++++++------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 115cabb2cd..e7236a5a48 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -41,6 +41,7 @@ @implementation WireguardTunnel { int m_tunfd; CFRunLoopTimerRef m_timer; + dispatch_queue_t m_dispatch; dispatch_group_t m_workqueue; struct timespec m_lastHandshake; @@ -70,6 +71,7 @@ - (id)init { m_tunfd = -1; m_wireguard = nil; + m_dispatch = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); _mtu = IPV6_MMTU; return self; @@ -180,7 +182,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options nw_parameters_t params = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); self.connection = nw_connection_create(options.serverIpv4Addr, params); - nw_connection_set_queue(self.connection, dispatch_get_main_queue()); + nw_connection_set_queue(self.connection, m_dispatch); // (Re)-create the Wireguard tunnel structure. if (m_wireguard) { @@ -302,7 +304,7 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { - (void) renegotiate { NSLog(@"wireguard renegotiate"); - uint8_t* handshake = (UInt8 *)malloc(WG_MAX_HANDSHAKE_SIZE); + uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); @@ -311,9 +313,7 @@ - (void) renegotiate { clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); m_handshakeTimeout.tv_sec += WG_MAX_HANDSHAKE_TIMEOUT; - NSData* data = [NSData dataWithBytesNoCopy:handshake - length:WG_MAX_HANDSHAKE_SIZE]; - [self handleWireguard:result withData:data]; + [self handleWireguard:result withBuffer:handshake]; } - (void) handleTimer { @@ -332,27 +332,23 @@ - (void) handleTimer { } } - uint8_t* handshake = (uint8_t *)malloc(WG_MAX_HANDSHAKE_SIZE); + uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); - - NSData* data = [NSData dataWithBytesNoCopy:handshake - length:WG_MAX_HANDSHAKE_SIZE]; - [self handleWireguard:result withData:data]; + [self handleWireguard:result withBuffer:handshake]; } - (void) startOutboundWorker { - dispatch_group_async(m_workqueue, - dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), - ^(void){ + dispatch_group_async(m_workqueue, m_dispatch, ^(void){ size_t mtu = self.mtu; - uint8_t buffer[mtu + 16]; + uint8_t plaintext[mtu + 16]; + uint8_t ciphertext[mtu + WG_PACKET_OVERHEAD]; uint32_t header; struct iovec iov[2]; iov[0].iov_base = &header; iov[0].iov_len = sizeof(header); - iov[1].iov_base = buffer; + iov[1].iov_base = plaintext; iov[1].iov_len = mtu; while (true) { @@ -379,19 +375,15 @@ - (void) startOutboundWorker { // no such padding during encryption. So let's do it manually ourself. int tail = pktlen % 16; if (tail) { - memset(buffer + pktlen, 0, 16 - tail); + memset(plaintext + pktlen, 0, 16 - tail); pktlen += 16 - tail; } // Encrypt the wireguard packet. - size_t enclength = mtu + WG_PACKET_OVERHEAD; - uint8_t *encrypt = (uint8_t *)malloc(enclength); struct wireguard_result result; - result = wireguard_write(m_wireguard, buffer, pktlen, encrypt, enclength); - - NSData* data = [NSData dataWithBytesNoCopy:encrypt - length:enclength]; - [self handleWireguard:result withData:data]; + result = wireguard_write(m_wireguard, plaintext, pktlen, ciphertext, + sizeof(ciphertext)); + [self handleWireguard:result withBuffer:ciphertext]; } }); } @@ -442,8 +434,8 @@ - (void) handleInbound { } } - NSData* packet = [NSData dataWithBytesNoCopy:plaintext length:length]; - [self handleWireguard:result withData:packet]; + [self handleWireguard:result withBuffer:plaintext]; + free(plaintext); // Keep going to receive more data. [self handleInbound]; @@ -451,28 +443,27 @@ - (void) handleInbound { } - (void) handleWireguard:(struct wireguard_result)result - withData:(NSData*)data { + withBuffer:(const uint8_t*)data { uint32_t header = htonl(AF_INET); switch (result.op) { case WIREGUARD_DONE: break; - case WRITE_TO_NETWORK: - if (result.size <= data.length) { - dispatch_data_t dgram = dispatch_data_create(data.bytes, result.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_DEFAULT); - nw_connection_send(self.connection, dgram, - NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, - ^(nw_error_t error) { - if (error) { - CFErrorRef cfError = nw_error_copy_cf_error(error); - NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); - CFRelease(cfError); - } - }); - } - break; + case WRITE_TO_NETWORK:{ + dispatch_data_t dgram = dispatch_data_create(data, result.size, + dispatch_get_main_queue(), + DISPATCH_DATA_DESTRUCTOR_DEFAULT); + nw_connection_send(self.connection, dgram, + NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, + ^(nw_error_t error) { + if (error) { + CFErrorRef cfError = nw_error_copy_cf_error(error); + NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); + CFRelease(cfError); + } + }); + } + break; case WIREGUARD_ERROR: NSLog(@"wireguard error: %zu", result.size); @@ -482,13 +473,11 @@ - (void) handleWireguard:(struct wireguard_result)result header = htonl(AF_INET6); [[fallthrough]]; case WRITE_TO_TUNNEL_IPV4: - if (result.size <= data.length) { - const struct iovec iov[2] = { - {.iov_base = &header, .iov_len = sizeof(header)}, - {.iov_base = (void *)data.bytes, .iov_len = result.size}, - }; - int err = writev(m_tunfd, iov, 2); - } + const struct iovec iov[2] = { + {.iov_base = &header, .iov_len = sizeof(header)}, + {.iov_base = (void *)data, .iov_len = result.size}, + }; + int err = writev(m_tunfd, iov, 2); break; } } From 72dbe24f0891482d90c1af975d65cd9d2bbeec8c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 24 Apr 2026 12:11:17 -0700 Subject: [PATCH 17/31] Refactor statusUpdated signal to use a ControllerStatus class --- src/connectionhealth.cpp | 17 +++------ src/connectionhealth.h | 8 ++-- src/controller.cpp | 41 +++++++++++---------- src/controller.h | 41 +++++++++++++++------ src/controllerimpl.cpp | 32 ---------------- src/controllerimpl.h | 12 +----- src/localsocketcontroller.cpp | 2 +- src/platforms/android/androidcontroller.cpp | 11 ++++-- src/platforms/ios/ioscontroller.mm | 10 +++-- src/platforms/linux/linuxcontroller.cpp | 2 +- src/platforms/linux/netmgrcontroller.cpp | 14 +++++-- src/platforms/macos/macoscontroller.mm | 2 +- src/platforms/wasm/wasmcontroller.cpp | 4 +- 13 files changed, 92 insertions(+), 104 deletions(-) diff --git a/src/connectionhealth.cpp b/src/connectionhealth.cpp index 506be008cb..9e181b9689 100644 --- a/src/connectionhealth.cpp +++ b/src/connectionhealth.cpp @@ -92,8 +92,8 @@ void ConnectionHealth::stop() { m_dnsPingTimer.stop(); } -void ConnectionHealth::startActive(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address) { +void ConnectionHealth::startActive(const QHostAddress& serverIpv4Gateway, + const QHostAddress& deviceIpv4Address) { logger.debug() << "ConnectionHealth active started"; bool isNotOnOrOnPartial = @@ -101,14 +101,14 @@ void ConnectionHealth::startActive(const QString& serverIpv4Gateway, MozillaVPN::instance()->controller()->state() != Controller::StateOnPartial; - if (serverIpv4Gateway.isEmpty() || isNotOnOrOnPartial) { + if (!serverIpv4Gateway.isNull() || isNotOnOrOnPartial) { logger.info() << "ConnectionHealth not starting because no connection"; return; } m_currentGateway = serverIpv4Gateway; m_deviceAddress = deviceIpv4Address; - m_pingHelper.start(serverIpv4Gateway, deviceIpv4Address); + m_pingHelper.start(serverIpv4Gateway.toString(), deviceIpv4Address.toString()); m_noSignalTimer.start(PING_TIME_NOSIGNAL); m_healthCheckTimer.start(PING_TIME_UNSTABLE); @@ -175,14 +175,9 @@ void ConnectionHealth::connectionStateChanged() { case Controller::StateOnPartial: case Controller::StateOn: MozillaVPN::instance()->controller()->getStatus( - [this](const QString& serverIpv4Gateway, - const QString& deviceIpv4Address, uint64_t txBytes, - uint64_t rxBytes) { - Q_UNUSED(txBytes); - Q_UNUSED(rxBytes); - + [this](const ControllerStatus& status) { stop(); - startActive(serverIpv4Gateway, deviceIpv4Address); + startActive(status.m_ipv4Gateway, status.m_ipv4Address); }); break; diff --git a/src/connectionhealth.h b/src/connectionhealth.h index 2637c8c736..9fbf8bcaf4 100644 --- a/src/connectionhealth.h +++ b/src/connectionhealth.h @@ -55,8 +55,8 @@ class ConnectionHealth final : public QObject { private: void stop(); - void startActive(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address); + void startActive(const QHostAddress& serverIpv4Gateway, + const QHostAddress& deviceIpv4Address); void startIdle(); void pingSentAndReceived(qint64 msec); @@ -89,8 +89,8 @@ class ConnectionHealth final : public QObject { bool m_dnsPingInitialized = false; bool m_suspended = false; - QString m_currentGateway; - QString m_deviceAddress; + QHostAddress m_currentGateway; + QHostAddress m_deviceAddress; #ifdef UNIT_TEST friend class TestConnectionHealth; diff --git a/src/controller.cpp b/src/controller.cpp index 2dd6732441..9d94687318 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -5,6 +5,8 @@ #include "controller.h" #include +#include +#include #include #include "constants.h" @@ -838,16 +840,10 @@ void Controller::cleanupBackendLogs() { } } -void Controller::getStatus( - std::function&& a_callback) { +void Controller::getStatus(std::function&& cb) { logger.debug() << "check status"; - std::function - callback = std::move(a_callback); + std::function callback = std::move(cb); bool requestStatus = m_getStatusCallbacks.isEmpty(); @@ -858,20 +854,13 @@ void Controller::getStatus( } } -void Controller::statusUpdated(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address, - uint64_t txBytes, uint64_t rxBytes) { +void Controller::statusUpdated(const ControllerStatus& status) { logger.debug() << "Status updated"; - QList > - list; + QList> list; list.swap(m_getStatusCallbacks); - for (const std::function&func : list) { - func(serverIpv4Gateway, deviceIpv4Address, txBytes, rxBytes); + for (const std::function&func : list) { + func(status); } } @@ -1171,3 +1160,17 @@ bool Controller::shouldSuppressNextNotification() { return false; } } + +ControllerStatus::ControllerStatus(const QJsonObject& obj) { + m_connected = obj.value("connected").toBool(); + if (m_connected) { + QString dateString = obj.value("date").toString(); + m_timestamp = QDateTime::fromString(dateString, Qt::ISODate); + } + m_ipv4Address = QHostAddress(obj.value("deviceIpv4Address").toString()); + m_ipv6Address = QHostAddress(obj.value("deviceIpv6Address").toString()); + m_ipv4Gateway = QHostAddress(obj.value("serverIpv4Gateway").toString()); + m_ipv6Gateway = QHostAddress(obj.value("serverIpv6Gateway").toString()); + m_rxBytes = obj.value("rxBytes").toInteger(); + m_txBytes = obj.value("txBytes").toInteger(); +} diff --git a/src/controller.h b/src/controller.h index d25b86414e..a3db05b0a9 100644 --- a/src/controller.h +++ b/src/controller.h @@ -20,6 +20,32 @@ class Controller; class ControllerImpl; +class ControllerStatus { + Q_GADGET + + Q_PROPERTY(bool connected MEMBER m_connected); + Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp); + Q_PROPERTY(QHostAddress ipv4Address MEMBER m_ipv4Address); + Q_PROPERTY(QHostAddress ipv6Address MEMBER m_ipv6Address); + Q_PROPERTY(QHostAddress ipv4Gateway MEMBER m_ipv4Gateway); + Q_PROPERTY(QHostAddress ipv6Gateway MEMBER m_ipv6Gateway); + Q_PROPERTY(quint64 rxBytes MEMBER m_rxBytes); + Q_PROPERTY(quint64 txBytes MEMBER m_txBytes); + + public: + ControllerStatus() {} + ControllerStatus(const QJsonObject& obj); + + bool m_connected; + QDateTime m_timestamp; + QHostAddress m_ipv4Address; + QHostAddress m_ipv6Address; + QHostAddress m_ipv4Gateway; + QHostAddress m_ipv6Gateway; + quint64 m_rxBytes; + quint64 m_txBytes; +}; + class Controller : public QObject, public LogSerializer { Q_OBJECT Q_DISABLE_COPY_MOVE(Controller) @@ -155,10 +181,7 @@ class Controller : public QObject, public LogSerializer { QString logName() const override { return "Mozilla VPN backend logs"; } void logSerialize(QIODevice* device) override; - void getStatus( - std::function&& callback); + void getStatus(std::function&& callback); QString currentServerString() const; @@ -197,9 +220,7 @@ class Controller : public QObject, public LogSerializer { void handshakeTimeout(); void connected(const QString& pubkey); void disconnected(); - void statusUpdated(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address, uint64_t txBytes, - uint64_t rxBytes); + void statusUpdated(const ControllerStatus& status); void implInitialized(bool status, bool connected, const QDateTime& connectionDate); void implPermRequired(); @@ -317,11 +338,7 @@ class Controller : public QObject, public LogSerializer { PingHelper m_pingCanary; bool m_pingReceived = false; - QList> - m_getStatusCallbacks; - + QList> m_getStatusCallbacks; }; // namespace Controller #endif // CONTROLLER_H diff --git a/src/controllerimpl.cpp b/src/controllerimpl.cpp index 9e682548cf..9940aed0e1 100644 --- a/src/controllerimpl.cpp +++ b/src/controllerimpl.cpp @@ -4,44 +4,12 @@ #include "controllerimpl.h" -#include -#include - #include "logger.h" namespace { Logger logger("ControllerImpl"); } // namespace -void ControllerImpl::emitStatusFromJson(const QJsonObject& obj) { - QJsonValue serverIpv4Gateway = obj.value("serverIpv4Gateway"); - if (!serverIpv4Gateway.isString()) { - logger.error() << "Unexpected serverIpv4Gateway value"; - return; - } - - QJsonValue deviceIpv4Address = obj.value("deviceIpv4Address"); - if (!deviceIpv4Address.isString()) { - logger.error() << "Unexpected deviceIpv4Address value"; - return; - } - - QJsonValue txBytes = obj.value("txBytes"); - if (!txBytes.isDouble()) { - logger.error() << "Unexpected txBytes value"; - return; - } - - QJsonValue rxBytes = obj.value("rxBytes"); - if (!rxBytes.isDouble()) { - logger.error() << "Unexpected rxBytes value"; - return; - } - - emit statusUpdated(serverIpv4Gateway.toString(), deviceIpv4Address.toString(), - txBytes.toDouble(), rxBytes.toDouble()); -} - void ControllerImpl::getBackendLogs(QIODevice* device) { QString name = metaObject()->className(); QString reply = QString("Backend logs are not supported with %1").arg(name); diff --git a/src/controllerimpl.h b/src/controllerimpl.h index 41c84eaeed..1ab40660e4 100644 --- a/src/controllerimpl.h +++ b/src/controllerimpl.h @@ -86,10 +86,6 @@ class ControllerImpl : public QObject { virtual void sendUpdatedConfig(InterfaceConfig& entryConfig, InterfaceConfig& exitConfig) {}; - protected: - // Helper method - process a JSON status and emit the statusUpdated signal. - void emitStatusFromJson(const QJsonObject& obj); - signals: // This signal is emitted when the controller is initialized. Note that the // VPN tunnel can be already active. In this case, "connected" should be set @@ -110,13 +106,7 @@ class ControllerImpl : public QObject { void disconnected(); // This method should be emitted after a checkStatus() call. - // "serverIpv4Gateway" is the current VPN tunnel gateway. - // "deviceIpv4Address" is the address of the VPN client. - // "txBytes" and "rxBytes" contain the number of transmitted and received - // bytes since the last statusUpdated signal. - void statusUpdated(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address, uint64_t txBytes, - uint64_t rxBytes); + void statusUpdated(const ControllerStatus& status); // This signal is emitted when the implementation encounters an error. void backendFailure(Controller::ErrorCode errorCode); diff --git a/src/localsocketcontroller.cpp b/src/localsocketcontroller.cpp index 76a1fac1aa..864ef17b6d 100644 --- a/src/localsocketcontroller.cpp +++ b/src/localsocketcontroller.cpp @@ -269,7 +269,7 @@ void LocalSocketController::parseCommand(const QByteArray& command) { } if (type == "status") { - emitStatusFromJson(obj); + emit statusUpdated(ControllerStatus(obj)); return; } diff --git a/src/platforms/android/androidcontroller.cpp b/src/platforms/android/androidcontroller.cpp index a2ab1b0cdf..4e3cd59de4 100644 --- a/src/platforms/android/androidcontroller.cpp +++ b/src/platforms/android/androidcontroller.cpp @@ -76,10 +76,13 @@ AndroidController::AndroidController() { activity, &AndroidVPNActivity::eventStatisticUpdate, this, [this](const QString& parcelBody) { auto doc = QJsonDocument::fromJson(parcelBody.toUtf8()); - emit statusUpdated(doc.object()["endpoint"].toString(), - doc.object()["deviceIpv4"].toString(), - doc.object()["tx_bytes"].toInt(), - doc.object()["rx_bytes"].toInt()); + ControllerStatus st; + st.m_connected = true; + st.m_ipv4Gateway = QHostAddress(doc.object()["endpoint"].toString()); + st.m_ipv4Address = QHostAddress(doc.object()["deviceIpv4"].toString()); + st.m_rxBytes = doc.object()["tx_bytes"].toInteger(); + st.m_txBytes = doc.object()["rx_bytes"].toInteger(); + emit statusUpdated(st); }, Qt::QueuedConnection); connect( diff --git a/src/platforms/ios/ioscontroller.mm b/src/platforms/ios/ioscontroller.mm index fc905949f8..63f0c31474 100644 --- a/src/platforms/ios/ioscontroller.mm +++ b/src/platforms/ios/ioscontroller.mm @@ -223,7 +223,7 @@ void IOSController::deleteOSTunnelConfig() { [impl deleteOSTunnelConfig]; } -void IOSController::checkStatus() { +void IOSController::checkStatus(QObject* receiver) { logger.debug() << "Checking status"; if (m_checkingStatus) { @@ -267,8 +267,12 @@ logger.debug() << "ServerIpv4Gateway:" << serverIpv4Gateway << "DeviceIpv4Address:" << deviceIpv4Address << "RxBytes:" << rxBytes << "TxBytes:" << txBytes; - emit statusUpdated(QString::fromNSString(serverIpv4Gateway), - QString::fromNSString(deviceIpv4Address), txBytes, rxBytes); + ControllerStatus st; + st.m_connected = true; + st.m_ipv4Gateway = QHostAddress(QString::fromNSString(serverIpv4Gateway)); + st.m_ipv4Address = QHostAddress(QString::fromNSString(deviceIpv4Address)); + st.m_rxBytes = rxBytes; + st.m_txBytes = txBytes; }]; } diff --git a/src/platforms/linux/linuxcontroller.cpp b/src/platforms/linux/linuxcontroller.cpp index 3337203bd4..e273e7ccac 100644 --- a/src/platforms/linux/linuxcontroller.cpp +++ b/src/platforms/linux/linuxcontroller.cpp @@ -211,7 +211,7 @@ void LinuxController::checkStatusCompleted(QDBusPendingCallWatcher* call) { return; } - emitStatusFromJson(obj); + emit statusUpdated(ControllerStatus(obj)); } void LinuxController::getBackendLogs(QIODevice* device) { diff --git a/src/platforms/linux/netmgrcontroller.cpp b/src/platforms/linux/netmgrcontroller.cpp index d4c7bb5ce9..5a6ccc1fbd 100644 --- a/src/platforms/linux/netmgrcontroller.cpp +++ b/src/platforms/linux/netmgrcontroller.cpp @@ -390,10 +390,16 @@ void NetmgrController::checkStatus() { QString("/sys/class/net/%1/statistics/tx_bytes").arg(WG_INTERFACE_NAME); QString rxPath = QString("/sys/class/net/%1/statistics/rx_bytes").arg(WG_INTERFACE_NAME); - uint64_t tx = readSysfsFile(txPath); - uint64_t rx = readSysfsFile(rxPath); - logger.info() << "Status:" << m_deviceIpv4Address << tx << rx; - emit statusUpdated(m_serverIpv4Gateway, m_deviceIpv4Address, tx, rx); + + ControllerStatus st; + st.m_connected = true; + st.m_timestamp = guessUptime(); + st.m_ipv4Gateway = m_serverIpv4Gateway; + st.m_ipv4Address = m_deviceIpv4Address; + st.m_rxBytes = readSysfsFile(rxPath); + st.m_txBytes = readSysfsFile(txPath); + + emit statusUpdated(st); } QDateTime NetmgrController::guessUptime() { diff --git a/src/platforms/macos/macoscontroller.mm b/src/platforms/macos/macoscontroller.mm index 7223787371..94e546d3e1 100644 --- a/src/platforms/macos/macoscontroller.mm +++ b/src/platforms/macos/macoscontroller.mm @@ -231,7 +231,7 @@ emit initialized(true, jsObj.value("connected").toBool(), [remoteObject() getStatus:^(NSString* status){ QByteArray jsBlob = QString::fromNSString(status).toUtf8(); QJsonObject obj = QJsonDocument::fromJson(jsBlob).object(); - emitStatusFromJson(obj); + emit statusUpdated(ControllerStatus(obj)); }]; } diff --git a/src/platforms/wasm/wasmcontroller.cpp b/src/platforms/wasm/wasmcontroller.cpp index bbb0f40a87..8543d051d4 100644 --- a/src/platforms/wasm/wasmcontroller.cpp +++ b/src/platforms/wasm/wasmcontroller.cpp @@ -42,4 +42,6 @@ void WasmController::deactivate() { QMetaObject::invokeMethod(m_mock, "deactivate", Qt::QueuedConnection); } -void WasmController::checkStatus() { emitStatusFromJson(m_mock->getStatus()); } +void WasmController::checkStatus() { + emit statusUpdated(ControllerStatus(m_mock->getStatus())); +} From 8e5ece3875a39d5be3191e64a293e6b7fa4ff16c Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 24 Apr 2026 13:43:42 -0700 Subject: [PATCH 18/31] Make the ControllerStatus a Q_PROPERTY and tidy up getStatus() callbacks --- src/connectionhealth.cpp | 28 +++++++++++++++------------- src/connectionhealth.h | 5 +++-- src/controller.cpp | 29 ++++++++++------------------- src/controller.h | 8 ++++++-- tests/qml/moccontroller.cpp | 11 +---------- tests/unit/moccontroller.cpp | 11 +---------- 6 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/connectionhealth.cpp b/src/connectionhealth.cpp index 9e181b9689..941fe979ee 100644 --- a/src/connectionhealth.cpp +++ b/src/connectionhealth.cpp @@ -92,8 +92,7 @@ void ConnectionHealth::stop() { m_dnsPingTimer.stop(); } -void ConnectionHealth::startActive(const QHostAddress& serverIpv4Gateway, - const QHostAddress& deviceIpv4Address) { +void ConnectionHealth::startActive(const ControllerStatus& status) { logger.debug() << "ConnectionHealth active started"; bool isNotOnOrOnPartial = @@ -101,14 +100,14 @@ void ConnectionHealth::startActive(const QHostAddress& serverIpv4Gateway, MozillaVPN::instance()->controller()->state() != Controller::StateOnPartial; - if (!serverIpv4Gateway.isNull() || isNotOnOrOnPartial) { + if (status.m_ipv4Gateway.isNull() || isNotOnOrOnPartial) { logger.info() << "ConnectionHealth not starting because no connection"; return; } - m_currentGateway = serverIpv4Gateway; - m_deviceAddress = deviceIpv4Address; - m_pingHelper.start(serverIpv4Gateway.toString(), deviceIpv4Address.toString()); + m_currentGateway = status.m_ipv4Gateway; + m_deviceAddress = status.m_ipv4Address; + m_pingHelper.start(m_currentGateway.toString(), m_deviceAddress.toString()); m_noSignalTimer.start(PING_TIME_NOSIGNAL); m_healthCheckTimer.start(PING_TIME_UNSTABLE); @@ -163,7 +162,8 @@ void ConnectionHealth::setStability(ConnectionStability stability) { } void ConnectionHealth::connectionStateChanged() { - Controller::State state = MozillaVPN::instance()->controller()->state(); + Controller* controller = MozillaVPN::instance()->controller(); + Controller::State state = controller->state(); logger.debug() << "Connection state changed to" << state; if ((state != Controller::StateInitializing) && @@ -174,11 +174,10 @@ void ConnectionHealth::connectionStateChanged() { switch (state) { case Controller::StateOnPartial: case Controller::StateOn: - MozillaVPN::instance()->controller()->getStatus( - [this](const ControllerStatus& status) { - stop(); - startActive(status.m_ipv4Gateway, status.m_ipv4Address); - }); + QObject::connect(controller, &Controller::statusUpdated, this, + &ConnectionHealth::startActive, + Qt::SingleShotConnection); + controller->refreshStatus(); break; case Controller::StateOff: @@ -264,7 +263,10 @@ void ConnectionHealth::applicationStateChanged(Qt::ApplicationState state) { m_suspended = false; logger.debug() << "Resuming connection check from suspension"; - startActive(m_currentGateway, m_deviceAddress); + ConnectionStatus st; + st.m_ipv4Address = m_deviceAddress; + st.m_ipv6Address = m_currentGateway + startActive(st); break; case Qt::ApplicationState::ApplicationSuspended: diff --git a/src/connectionhealth.h b/src/connectionhealth.h index 9fbf8bcaf4..97cb8d228b 100644 --- a/src/connectionhealth.h +++ b/src/connectionhealth.h @@ -8,6 +8,8 @@ #include "dnspingsender.h" #include "pinghelper.h" +class ControllerStatus; + // The baseline latency measurement averaged using an Exponentially Weighted // Moving Average (EWMA), this defines the decay rate. constexpr uint32_t PING_BASELINE_EWMA_DIVISOR = 8; @@ -55,8 +57,7 @@ class ConnectionHealth final : public QObject { private: void stop(); - void startActive(const QHostAddress& serverIpv4Gateway, - const QHostAddress& deviceIpv4Address); + void startActive(const ControllerStatus& status); void startIdle(); void pingSentAndReceived(qint64 msec); diff --git a/src/controller.cpp b/src/controller.cpp index 9d94687318..bcd12dfcac 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -167,7 +167,11 @@ void Controller::initialize() { connect(m_impl.get(), &ControllerImpl::permissionRequired, this, &Controller::implPermRequired); connect(m_impl.get(), &ControllerImpl::statusUpdated, this, - &Controller::statusUpdated); + [this](const ControllerStatus& status) { + logger.debug() << "Status updated"; + m_status = status; + emit statusUpdated(status); + }); connect(m_impl.get(), &ControllerImpl::backendFailure, this, &Controller::handleBackendFailure); connect(this, &Controller::stateChanged, this, @@ -840,27 +844,14 @@ void Controller::cleanupBackendLogs() { } } -void Controller::getStatus(std::function&& cb) { +void Controller::refreshStatus() { logger.debug() << "check status"; - std::function callback = std::move(cb); - - bool requestStatus = m_getStatusCallbacks.isEmpty(); - - m_getStatusCallbacks.append(std::move(callback)); - - if (m_impl && requestStatus) { + if (m_impl) { m_impl->checkStatus(); - } -} - -void Controller::statusUpdated(const ControllerStatus& status) { - logger.debug() << "Status updated"; - QList> list; - - list.swap(m_getStatusCallbacks); - for (const std::function&func : list) { - func(status); + } else { + // Nothing changed - but emit it anyways. + emit statusUpdated(m_status); } } diff --git a/src/controller.h b/src/controller.h index a3db05b0a9..c5ac4efb4c 100644 --- a/src/controller.h +++ b/src/controller.h @@ -160,6 +160,7 @@ class Controller : public QObject, public LogSerializer { Q_INVOKABLE bool isActive() const { return m_state > StateOff; } const ServerData& currentServer() const { return m_serverData; } + const ControllerStatus& getStatus() const { return m_status; } bool enableDisconnectInConfirming() const { return m_enableDisconnectInConfirming; @@ -181,7 +182,7 @@ class Controller : public QObject, public LogSerializer { QString logName() const override { return "Mozilla VPN backend logs"; } void logSerialize(QIODevice* device) override; - void getStatus(std::function&& callback); + void refreshStatus(); QString currentServerString() const; @@ -215,12 +216,13 @@ class Controller : public QObject, public LogSerializer { // This is just for testing purposes. Q_PROPERTY(QString currentServerString READ currentServerString NOTIFY currentServerChanged); + + Q_PROPERTY(ControllerStatus status READ getStatus NOTIFY statusUpdated); private slots: void handshakeTimeout(); void connected(const QString& pubkey); void disconnected(); - void statusUpdated(const ControllerStatus& status); void implInitialized(bool status, bool connected, const QDateTime& connectionDate); void implPermRequired(); @@ -229,6 +231,7 @@ class Controller : public QObject, public LogSerializer { signals: void stateChanged(); void errorChanged(); + void statusUpdated(const ControllerStatus& status); void timestampChanged(); void enableDisconnectInConfirmingChanged(); void connectionRetryChanged(); @@ -305,6 +308,7 @@ class Controller : public QObject, public LogSerializer { QTimer m_handshakeTimer; QDateTime m_connectedTimeInUTC; + ControllerStatus m_status; State m_state = StateInitializing; ActivationPrincipal m_initiator = Null; diff --git a/tests/qml/moccontroller.cpp b/tests/qml/moccontroller.cpp index 3fbed29585..854e74b262 100644 --- a/tests/qml/moccontroller.cpp +++ b/tests/qml/moccontroller.cpp @@ -61,16 +61,7 @@ Controller::ErrorCode Controller::error() const { void Controller::updateRequired() {} -void Controller::getStatus( - std::function&& a_callback) { - std::function - callback = std::move(a_callback); - callback("127.0.0.1", "127.0.0.1", 0, 0); -} +void Controller::refreshStatus() { emit connectionStatusChanged(m_status); } void Controller::quit() {} diff --git a/tests/unit/moccontroller.cpp b/tests/unit/moccontroller.cpp index 70ce22a166..874de49346 100644 --- a/tests/unit/moccontroller.cpp +++ b/tests/unit/moccontroller.cpp @@ -67,16 +67,7 @@ Controller::ErrorCode Controller::error() const { void Controller::updateRequired() {} -void Controller::getStatus( - std::function&& a_callback) { - std::function - callback = std::move(a_callback); - callback("127.0.0.1", "127.0.0.1", 0, 0); -} +void Controller::refreshStatus() { emit connectionStatusChanged(m_status); } void Controller::quit() {} From 238f9d8f519a874660fac6ed1d7967daca883422 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Fri, 24 Apr 2026 17:30:52 -0700 Subject: [PATCH 19/31] Add logger support for NSError --- src/utils/logger.cpp | 12 +++++++++--- src/utils/logger.h | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/utils/logger.cpp b/src/utils/logger.cpp index 4ad7e333ea..f047685bba 100644 --- a/src/utils/logger.cpp +++ b/src/utils/logger.cpp @@ -56,16 +56,22 @@ Logger::Log& Logger::Log::operator<<(QTextStreamFunction t) { #ifdef Q_OS_APPLE Logger::Log& Logger::Log::operator<<(const NSString* t) { - m_data->m_ts << QString::fromNSString(t); + m_data->m_ts << QString::fromNSString(t) << ' '; + return *this; +} +Logger::Log& Logger::Log::operator<<(const NSError* t) { + CFStringRef ref = CFErrorCopyDescription((CFErrorRef)t); + m_data->m_ts << QString::fromCFString(ref) << ' '; + CFRelease(ref); return *this; } Logger::Log& Logger::Log::operator<<(CFStringRef t) { - m_data->m_ts << QString::fromCFString(t); + m_data->m_ts << QString::fromCFString(t) << ' '; return *this; } Logger::Log& Logger::Log::operator<<(CFErrorRef t) { CFStringRef ref = CFErrorCopyDescription(t); - m_data->m_ts << QString::fromCFString(ref); + m_data->m_ts << QString::fromCFString(ref) << ' '; CFRelease(ref); return *this; } diff --git a/src/utils/logger.h b/src/utils/logger.h index 8c958890aa..649abcceae 100644 --- a/src/utils/logger.h +++ b/src/utils/logger.h @@ -14,6 +14,8 @@ #ifdef Q_OS_APPLE # include +# include +Q_FORWARD_DECLARE_OBJC_CLASS(NSError); #endif class QJsonObject; @@ -39,6 +41,7 @@ class Logger { Log& operator<<(const void* t); #ifdef Q_OS_APPLE Log& operator<<(const NSString* t); + Log& operator<<(const NSError* t); Log& operator<<(CFStringRef t); Log& operator<<(CFErrorRef t); #endif From 6f130f16878adf7f6b7d01637c4474d622a52617 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 27 Apr 2026 14:15:53 -0700 Subject: [PATCH 20/31] Implement status reporting via handleAppMessage --- macos/networkextension/CMakeLists.txt | 2 + .../VPNSplitTunnelProvider.mm | 25 ++-- macos/networkextension/interfaceconfig.h | 8 +- macos/networkextension/interfaceconfig.mm | 110 +++++++++--------- macos/networkextension/wireguardstatus.h | 20 ++++ macos/networkextension/wireguardstatus.mm | 38 ++++++ macos/networkextension/wireguardtunnel.h | 13 +-- macos/networkextension/wireguardtunnel.mm | 48 +++++++- .../macos/macosextensioncontroller.h | 4 + .../macos/macosextensioncontroller.mm | 35 +++++- 10 files changed, 216 insertions(+), 87 deletions(-) create mode 100644 macos/networkextension/wireguardstatus.h create mode 100644 macos/networkextension/wireguardstatus.mm diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index e67f76d8ad..ea21007bd9 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -73,6 +73,8 @@ target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/main.mm ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.h ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.mm + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.h + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.mm ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.h ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.mm ${CMAKE_CURRENT_SOURCE_DIR}/VPNSplitTunnelProvider.mm diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index 4302facc5a..92792ba210 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -157,7 +157,7 @@ - (void)startProxyWithOptions:(NSDictionary *)options m_handledUnknown = 0; // Parse the configuration - InterfaceConfig* config = [InterfaceConfig parseFromDict:options]; + InterfaceConfig* config = [[InterfaceConfig alloc] initFromDict:options]; if (!config) { completionHandler([VPNSplitTunnelProvider makeError:1 withDescription:@"invalid configuration"]); @@ -435,18 +435,11 @@ - (void)handleAppMessage:(NSData *)messageData // Wireguard Tunnel messages if ([action isEqualToString:@"status"]) { - NSError* err; - NSData* reply = [NSKeyedArchiver archivedDataWithRootObject:self.wireguard.status - requiringSecureCoding:YES - error:&err]; - if (err == nil) { - [VPNSplitTunnelProvider sendAppError:err completionHandler:completionHandler]; - } else { - [VPNSplitTunnelProvider sendAppResponse:reply completionHandler:completionHandler]; - } + [VPNSplitTunnelProvider sendAppObject:self.wireguard.status + completionHandler:completionHandler]; return; } - + // Application exclusion messages. if ([action isEqualToString: @"clear"]) { [self.vpnDisabledApps removeAllObjects]; @@ -469,6 +462,16 @@ + (void)sendAppResponse:(NSData*) responseData completionHandler(responseData); } ++ (void)sendAppObject:(id) obj + completionHandler:(void (^)(NSData*)) completionHandler { + NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; + if ([obj respondsToSelector:@selector(encodeWithCoder:)]) { + [obj encodeWithCoder: encoder]; + } + [encoder finishEncoding]; + completionHandler(encoder.encodedData); +} + + (void)sendAppError:(NSError*) error completionHandler:(void (^)(NSData*)) completionHandler { if (!completionHandler) { diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h index ffac32a394..d5f6f4692d 100644 --- a/macos/networkextension/interfaceconfig.h +++ b/macos/networkextension/interfaceconfig.h @@ -15,17 +15,19 @@ @interface InterfaceConfig : NSObject -+ (id)parseFromDict:(NSDictionary *)dict; -+ (id)parseFromCoder:(NSCoder*)coder; +- (id)initFromDict:(NSDictionary *)dict; +- (id)initFromCoder:(NSCoder*)coder; @property (strong) NSString* privateKey; @property (strong) nw_endpoint_t deviceIpv4Addr; @property (strong) nw_endpoint_t deviceIpv6Addr; @property (strong) NSString* serverPublicKey; -@property NSUInteger serverPort; +@property (assign) NSUInteger serverPort; @property (strong) nw_endpoint_t serverIpv4Addr; @property (strong) nw_endpoint_t serverIpv6Addr; +@property (strong) nw_endpoint_t serverIpv4Gateway; +@property (strong) nw_endpoint_t serverIpv6Gateway; @property (strong) NSArray* routes; diff --git a/macos/networkextension/interfaceconfig.mm b/macos/networkextension/interfaceconfig.mm index af2e32a869..688f447dea 100644 --- a/macos/networkextension/interfaceconfig.mm +++ b/macos/networkextension/interfaceconfig.mm @@ -61,7 +61,6 @@ - (const struct sockaddr*)getDestination { @end @implementation InterfaceConfig - - (NSString*)findString:(NSString*)key { NSObject* value = [self.dict objectForKey:key]; if (value == nil) { @@ -73,11 +72,41 @@ - (NSString*)findString:(NSString*)key { return (NSString*)value; } -+ (id)parseFromCoder:(NSCoder*)coder { - return [InterfaceConfig parseFromDict:[[NSDictionary alloc] initWithCoder:coder]]; +- (nw_endpoint_t)findAddress:(NSString*)key + withPort:(NSUInteger)port { + NSString* addr = [self findString:key]; + if (!addr) { + return nil; + } + // Strip the prefix length, if present. + addr = [addr componentsSeparatedByString:@"/"][0]; + + if ([addr containsString:@":"]) { + // IPv6 address + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(struct sockaddr_in6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(struct sockaddr_in6); + sin6.sin6_port = htons(port); + if (!inet_pton(AF_INET6, addr.UTF8String, &sin6.sin6_addr.s6_addr)) { + return nil; + } + return nw_endpoint_create_address((struct sockaddr*)&sin6); + } else { + // IPv4 address + struct sockaddr_in sin; + memset(&sin, 0, sizeof(struct sockaddr_in)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(struct sockaddr_in); + sin.sin_port = htons(port); + if (!inet_pton(AF_INET, addr.UTF8String, &sin.sin_addr.s_addr)) { + return nil; + } + return nw_endpoint_create_address((struct sockaddr*)&sin); + } } -+ (id)parseFromDict:(NSDictionary *)dict { +- (id)parseFromDict:(NSDictionary *)dict { InterfaceConfig* config = [InterfaceConfig new]; config->_dict = dict; @@ -85,35 +114,15 @@ + (id)parseFromDict:(NSDictionary *)dict { if (!config.privateKey) { return nil; } - NSString* deviceIpv4Addr = [config findString:@"deviceIpv4Addr"]; - if (!deviceIpv4Addr) { + config.deviceIpv4Addr = [config findAddress:@"deviceIpv4Addr" withPort:0]; + if ((!config.deviceIpv4Addr) || + (nw_endpoint_get_address(config.deviceIpv4Addr)->sa_family != AF_INET)) { return nil; - } else { - struct sockaddr_in sin; - NSString* addr = [deviceIpv4Addr componentsSeparatedByString:@"/"][0]; - - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_len = sizeof(sin); - if (!inet_pton(AF_INET, addr.UTF8String, &sin.sin_addr.s_addr)) { - return nil; - } - config.deviceIpv4Addr = nw_endpoint_create_address((struct sockaddr*)&sin); } - NSString* deviceIpv6Addr = [config findString:@"deviceIpv6Addr"]; - if (!deviceIpv6Addr) { + config.deviceIpv6Addr = [config findAddress:@"deviceIpv6Addr" withPort:0]; + if ((!config.deviceIpv6Addr) || + (nw_endpoint_get_address(config.deviceIpv6Addr)->sa_family != AF_INET6)) { return nil; - } else { - struct sockaddr_in6 sin6; - NSString* addr = [deviceIpv6Addr componentsSeparatedByString:@"/"][0]; - - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(sin6); - if (!inet_pton(AF_INET6, addr.UTF8String, &sin6.sin6_addr.s6_addr)) { - return nil; - } - config.deviceIpv6Addr = nw_endpoint_create_address((struct sockaddr*)&sin6); } config.serverPublicKey = [config findString:@"serverPublicKey"]; @@ -131,33 +140,12 @@ + (id)parseFromDict:(NSDictionary *)dict { config.serverPort = 51820; } - NSString* serverIpv4Addr = [config findString:@"serverIpv4AddrIn"]; - if (serverIpv4Addr) { - struct sockaddr_in sin; - memset(&sin, 0, sizeof(sin)); - sin.sin_family = AF_INET; - sin.sin_len = sizeof(sin); - sin.sin_port = htons(config.serverPort); - if (!inet_pton(AF_INET, serverIpv4Addr.UTF8String, &sin.sin_addr.s_addr)) { - return nil; - } + config.serverIpv4Addr = [config findAddress:@"serverIpv4AddrIn" withPort:config.serverPort]; + config.serverIpv6Addr = [config findAddress:@"serverIpv6AddrIn" withPort:config.serverPort]; - config.serverIpv4Addr = nw_endpoint_create_address((struct sockaddr*)&sin); - } - - NSString* serverIpv6Addr = [config findString:@"serverIpv6AddrIn"]; - if (serverIpv6Addr) { - struct sockaddr_in6 sin6; - memset(&sin6, 0, sizeof(sin6)); - sin6.sin6_family = AF_INET6; - sin6.sin6_len = sizeof(sin6); - sin6.sin6_port = htons(config.serverPort); - if (!inet_pton(AF_INET6, serverIpv6Addr.UTF8String, &sin6.sin6_addr.s6_addr)) { - return nil; - } - - config.serverIpv6Addr = nw_endpoint_create_address((struct sockaddr*)&sin6); - } + // We don't actually use this, but parse it anyways. + config.serverIpv4Gateway = [config findAddress:@"serverIpv4Gateway" withPort:0]; + config.serverIpv6Gateway = [config findAddress:@"serverIpv6Gateway" withPort:0]; // Parse the allowed IP address ranges. NSObject* rangesObject = [config.dict objectForKey:@"routes"]; @@ -177,4 +165,14 @@ + (id)parseFromDict:(NSDictionary *)dict { return config; } +- (id)initFromCoder:(NSCoder*)coder { + self = [super init]; + return [self parseFromDict:[[NSDictionary alloc] initWithCoder:coder]]; +} + +- (id)initFromDict:(NSDictionary *)dict { + self = [super init]; + return [self parseFromDict:dict]; +} + @end diff --git a/macos/networkextension/wireguardstatus.h b/macos/networkextension/wireguardstatus.h new file mode 100644 index 0000000000..ec2444a882 --- /dev/null +++ b/macos/networkextension/wireguardstatus.h @@ -0,0 +1,20 @@ +/* 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/. */ + +#import + +@interface WireguardStatus : NSObject + +@property (strong) NSDate* lastHandshake; +@property (strong) NSString* ipv4address; +@property (strong) NSString* ipv6address; +@property (strong) NSString* ipv4gateway; +@property (strong) NSString* ipv6gateway; +@property (assign) NSUInteger txBytes; +@property (assign) NSUInteger rxBytes; +@property (assign) float estimatedLoss; +@property (assign) NSUInteger estimatedRtt; + +- (void)encodeWithCoder:(NSCoder *)coder; +@end diff --git a/macos/networkextension/wireguardstatus.mm b/macos/networkextension/wireguardstatus.mm new file mode 100644 index 0000000000..7398884988 --- /dev/null +++ b/macos/networkextension/wireguardstatus.mm @@ -0,0 +1,38 @@ +/* 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 "wireguardstatus.h" + +@implementation WireguardStatus ++ (BOOL)supportsSecureCoding { + return YES; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.lastHandshake forKey:@"lastHandshake"]; + [coder encodeObject:self.ipv4address forKey:@"ipv4address"]; + [coder encodeObject:self.ipv6address forKey:@"ipv6address"]; + [coder encodeObject:self.ipv4gateway forKey:@"ipv4gateway"]; + [coder encodeObject:self.ipv6gateway forKey:@"ipv6gateway"]; + [coder encodeInt64:self.rxBytes forKey:@"rxBytes"]; + [coder encodeInt64:self.txBytes forKey:@"txBytes"]; + [coder encodeFloat:self.estimatedLoss forKey:@"estimatedLoss"]; + [coder encodeInt:self.estimatedRtt forKey:@"estimatedRtt"]; +} + +- (id)initWithCoder:(NSCoder*)coder { + self = [super init]; + self.lastHandshake = [coder decodeObjectOfClass:[NSDate class] forKey:@"lastHandshake"]; + self.ipv4address = [coder decodeObjectOfClass:[NSString class] forKey:@"ipv4address"]; + self.ipv6address = [coder decodeObjectOfClass:[NSString class] forKey:@"ipv6address"]; + self.ipv4gateway = [coder decodeObjectOfClass:[NSString class] forKey:@"ipv4gateway"]; + self.ipv6gateway = [coder decodeObjectOfClass:[NSString class] forKey:@"ipv6gateway"]; + self.rxBytes = [coder decodeInt64ForKey:@"rxBytes"]; + self.txBytes = [coder decodeInt64ForKey:@"txBytes"]; + self.estimatedLoss = [coder decodeFloatForKey:@"estimatedLoss"]; + self.estimatedRtt = [coder decodeIntForKey:@"estimatedRtt"]; + return self; +} + +@end diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h index f13ebb3c58..18394877d9 100644 --- a/macos/networkextension/wireguardtunnel.h +++ b/macos/networkextension/wireguardtunnel.h @@ -7,14 +7,7 @@ #import #import "interfaceconfig.h" - -@interface WireguardStats : NSObject -@property (strong) NSDate *lastHandshake; -@property NSUInteger txBytes; -@property NSUInteger rxBytes; -@property float estimatedLoss; -@property NSUInteger estimatedRtt; -@end +#import "wireguardstatus.h" @interface WireguardTunnel : NSObject @@ -29,7 +22,9 @@ - (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; @property (nonatomic) NSUInteger mtu; -@property (strong, readonly, getter=getStatus) WireguardStats* status; +@property (strong, readonly, getter=getStatus) WireguardStatus* status; +@property (strong) nw_endpoint_t ipv4address; +@property (strong) nw_endpoint_t ipv6address; @property (strong) nw_connection_t connection; @property (strong) nw_interface_t virtualInterface; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index e7236a5a48..cc4ad07b5a 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -32,10 +32,6 @@ // Private method in the network framework extern "C" nw_interface_t nw_interface_create_with_index_and_name(int ifindex, const char *ifname); -@implementation WireguardStats -// Nothing to see here. -@end - @implementation WireguardTunnel { struct wireguard_tunnel* m_wireguard; @@ -47,6 +43,10 @@ @implementation WireguardTunnel { struct timespec m_lastHandshake; struct timespec m_handshakeTimeout; + // Not used for anything, we just hold them for status generation. + nw_endpoint_t m_ipv4gateway; + nw_endpoint_t m_ipv6gateway; + // The completion handler to run on initial handshake or timeout. void (^m_completionHandler)(NSError *error); } @@ -106,6 +106,8 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options completionHandler:(void (^)(NSError *error)) completionHandler { m_completionHandler = completionHandler; m_workqueue = dispatch_group_create(); + m_ipv4gateway = options.serverIpv4Gateway; + m_ipv6gateway = options.serverIpv6Gateway; m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (m_tunfd < 0) { @@ -276,6 +278,8 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { if (ioctl(sock, SIOCAIFADDR, &ifr) < 0) { err = errorFromErrno(errno, @"failed to set tunnel address"); + } else { + self.ipv4address = endpoint; } } else if (sa->sa_family == AF_INET6) { struct in6_aliasreq ifr6; @@ -293,6 +297,8 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { if (ioctl(sock, SIOCAIFADDR_IN6, &ifr6) < 0) { err = errorFromErrno(errno, @"failed to set tunnel address"); + } else { + self.ipv6address = endpoint; } } else { // We don't recognize this address type. @@ -532,8 +538,8 @@ - (void)cancelTunnelWithError:(NSError*)error { [self shutdownTunnel:error]; } -- (WireguardStats*)getStatus { - WireguardStats* result = [WireguardStats new]; +- (WireguardStatus*)getStatus { + WireguardStatus* result = [WireguardStatus new]; // Get the handshake time from the timestamp of the last received handshake // response packet, this will yeild better precision than the stats we get @@ -546,6 +552,36 @@ - (WireguardStats*)getStatus { result.lastHandshake = [NSDate dateWithTimeIntervalSinceNow:diff]; } + // Get the interface addresses. + if (self.ipv4address) { + char* addrstr = nw_endpoint_copy_address_string(self.ipv4address); + result.ipv4address = [[NSString alloc] initWithBytesNoCopy:addrstr + length:strlen(addrstr) + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; + } + if (self.ipv6address) { + char* addrstr = nw_endpoint_copy_address_string(self.ipv6address); + result.ipv6address = [[NSString alloc] initWithBytesNoCopy:addrstr + length:strlen(addrstr) + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; + } + if (m_ipv4gateway) { + char* addrstr = nw_endpoint_copy_address_string(m_ipv4gateway); + result.ipv4gateway = [[NSString alloc] initWithBytesNoCopy:addrstr + length:strlen(addrstr) + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; + } + if (m_ipv6gateway) { + char* addrstr = nw_endpoint_copy_address_string(m_ipv6gateway); + result.ipv6gateway = [[NSString alloc] initWithBytesNoCopy:addrstr + length:strlen(addrstr) + encoding:NSASCIIStringEncoding + freeWhenDone:YES]; + } + // Fetch the rest of the stats directly from boringtun. struct stats wgStats = wireguard_stats(m_wireguard); result.txBytes = wgStats.tx_bytes; diff --git a/src/platforms/macos/macosextensioncontroller.h b/src/platforms/macos/macosextensioncontroller.h index 2cac719b1c..d78418a7a1 100644 --- a/src/platforms/macos/macosextensioncontroller.h +++ b/src/platforms/macos/macosextensioncontroller.h @@ -12,6 +12,7 @@ Q_FORWARD_DECLARE_OBJC_CLASS(MacOSExtensionDelegate); Q_FORWARD_DECLARE_OBJC_CLASS(NETransparentProxyManager); Q_FORWARD_DECLARE_OBJC_CLASS(NETunnelProviderSession); +Q_FORWARD_DECLARE_OBJC_CLASS(NSCoder); class MacOSExtensionController final : public ControllerImpl { Q_OBJECT @@ -41,6 +42,9 @@ class MacOSExtensionController final : public ControllerImpl { private: static NSString* extIdentifier(); + static QString parseArchivedString(NSCoder* archive, NSString* key); + static QHostAddress parseArchivedAddress(NSCoder* archive, NSString* key); + private: QString m_serverPublicKey; diff --git a/src/platforms/macos/macosextensioncontroller.mm b/src/platforms/macos/macosextensioncontroller.mm index 6b9e065362..b95187911c 100644 --- a/src/platforms/macos/macosextensioncontroller.mm +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -214,6 +214,17 @@ - (void)notifyStatusChanged:(NSNotification*)notify; } } +QString MacOSExtensionController::parseArchivedString(NSCoder* archive, + NSString* key) { + NSString* s = [archive decodeObjectOfClass:[NSString class] forKey:key]; + return QString::fromNSString(s); +} + +QHostAddress MacOSExtensionController::parseArchivedAddress(NSCoder* archive, + NSString* key) { + return QHostAddress(parseArchivedString(archive, key)); +} + void MacOSExtensionController::checkStatus() { if (!m_session) { // Extension is not running. @@ -228,11 +239,31 @@ - (void)notifyStatusChanged:(NSNotification*)notify; [m_session sendProviderMessage:msg.encodedData returnError:&error responseHandler:^(NSData* response){ - // TODO: Parse the status and emit something. + NSError* decodeError = nil; + NSKeyedUnarchiver* archive = [NSKeyedUnarchiver alloc]; + if (![archive initForReadingFromData:response error:&decodeError]) { + logger.debug() << "status decode failed:" << decodeError; + return; + } + + ControllerStatus st; + NSDate* timestamp = [archive decodeObjectOfClass:[NSDate class] forKey:@"lastHandshake"]; + if (timestamp) { + st.m_connected = true; + st.m_timestamp = QDateTime::fromCFDate((CFDateRef)timestamp); + } + st.m_ipv4Gateway = parseArchivedAddress(archive, @"ipv4gateway"); + st.m_ipv6Gateway = parseArchivedAddress(archive, @"ipv6gateway"); + st.m_ipv4Address = parseArchivedAddress(archive, @"ipv4address"); + st.m_ipv6Address = parseArchivedAddress(archive, @"ipv6address"); + st.m_rxBytes = [archive decodeInt64ForKey:@"rxBytes"]; + st.m_txBytes = [archive decodeInt64ForKey:@"txBytes"]; + emit statusUpdated(st); }]; if (error != nil) { - logger.debug() << "Split tunneling status failed:" << error; + logger.debug() << "status request failed:" << error; + emit statusUpdated(ControllerStatus()); return; } } From e241892c9bd8aeb9e1f22496f5ec57be97eb0adb Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Mon, 27 Apr 2026 14:24:16 -0700 Subject: [PATCH 21/31] The RouteManager is no longer necessary --- macos/networkextension/CMakeLists.txt | 2 - .../VPNSplitTunnelProvider.mm | 46 +- macos/networkextension/main.mm | 2 - macos/networkextension/routemanager.h | 27 - macos/networkextension/routemanager.mm | 511 ------------------ macos/networkextension/wireguardtunnel.mm | 127 +++++ 6 files changed, 128 insertions(+), 587 deletions(-) delete mode 100644 macos/networkextension/routemanager.h delete mode 100644 macos/networkextension/routemanager.mm diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index ea21007bd9..43be5845ad 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -71,8 +71,6 @@ target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.h ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.mm ${CMAKE_CURRENT_SOURCE_DIR}/main.mm - ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.h - ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.mm ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.h ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.mm ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.h diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index 92792ba210..a2dab0b973 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -7,7 +7,6 @@ #import "bypasstcpflow.h" #import "bypassudpflow.h" #import "interfaceconfig.h" -#import "routemanager.h" #import "wireguardtunnel.h" #include @@ -19,7 +18,7 @@ #include #include -@interface VPNSplitTunnelProvider : NETransparentProxyProvider +@interface VPNSplitTunnelProvider : NETransparentProxyProvider - (void)startProxyWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler; @@ -31,12 +30,7 @@ - (BOOL)handleNewFlow:(NEAppProxyFlow *)flow; - (BOOL)matchAppFlow:(NEAppProxyFlow *)flow; -- (void)defaultRouteChanged:(int)family - viaInterface:(nw_interface_t)interface - withGateway:(NSData*)gateway; - @property (strong) NETransparentProxyNetworkSettings* settings; -@property (strong) RouteManager* routeManager; @property (strong) WireguardTunnel* wireguard; @property (strong) InterfaceConfig* config; @@ -165,10 +159,6 @@ - (void)startProxyWithOptions:(NSDictionary *)options } _config = config; - // Start the route manager - _routeManager = [RouteManager new]; - [self.routeManager startWithDelegate:self]; - self.wireguard = [WireguardTunnel new]; self.settings = [[NETransparentProxyNetworkSettings alloc] initWithTunnelRemoteAddress:self.protocolConfiguration.serverAddress]; @@ -257,18 +247,6 @@ - (void)startProxyWithOptions:(NSDictionary *)options return; } - // Configure routes into the tunnel interface. - // Note that the default route should set RTF_IFSCOPE so that we - // leave the real default route untouched. - for (RoutePrefix* prefix in weakSelf.config.routes) { - [weakSelf.routeManager rtmSendRoute:RTM_ADD - toDestination:prefix.destination - withPrefix:prefix.prefixLength - viaInterface:weakSelf.wireguard.virtualInterface - withGateway:nil - andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; - } - // Success completionHandler(nil); }]; @@ -280,8 +258,6 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler { NSLog(@"stopping proxy"); - self.routeManager = nil; - NSLog(@"handled tcp flows: %lld", std::atomic_load(&m_handledTcpFlows)); NSLog(@"handled udp flows: %lld", std::atomic_load(&m_handledUdpFlows)); NSLog(@"handled unknown flows: %lld", std::atomic_load(&m_handledUnknown)); @@ -483,24 +459,4 @@ + (void)sendAppError:(NSError*) error completionHandler(encoder.encodedData); } -- (void)defaultRouteChanged:(int)family - viaInterface:(nw_interface_t)interface - withGateway:(NSData*)gateway { - int ifindex = interface ? nw_interface_get_index(interface) : 0; - - if (family == AF_INET) { - if (interface) { - NSLog(@"default ipv4 route via %s", nw_interface_get_name(interface)); - } else { - NSLog(@"default ipv4 route lost"); - } - } else if (family == AF_INET6) { - if (interface) { - NSLog(@"default ipv6 route via %s", nw_interface_get_name(interface)); - } else { - NSLog(@"default ipv6 route lost"); - } - } -} - @end diff --git a/macos/networkextension/main.mm b/macos/networkextension/main.mm index 4909b67486..5c0aa80969 100644 --- a/macos/networkextension/main.mm +++ b/macos/networkextension/main.mm @@ -5,8 +5,6 @@ #import #import -#import "routemanager.h" - int main(int argc, char *argv[]) { @autoreleasepool { diff --git a/macos/networkextension/routemanager.h b/macos/networkextension/routemanager.h deleted file mode 100644 index 45a0336bf3..0000000000 --- a/macos/networkextension/routemanager.h +++ /dev/null @@ -1,27 +0,0 @@ -/* 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/. */ - -#import -#import - -struct sockaddr; - -@protocol RouteManagerDelegate -- (void)defaultRouteChanged:(int)family - viaInterface:(nw_interface_t)interface - withGateway:(NSData*)gateway; -@end - -@interface RouteManager : NSObject -- (id)init; - -- (void)startWithDelegate:(NSObject*)delegate; - -- (void)rtmSendRoute:(int)action - toDestination:(const struct sockaddr*)dst - withPrefix:(unsigned int)plen - viaInterface:(nw_interface_t)ifindex - withGateway:(NSData*)gateway - andFlags:(int)flags; -@end diff --git a/macos/networkextension/routemanager.mm b/macos/networkextension/routemanager.mm deleted file mode 100644 index 4ac7f67612..0000000000 --- a/macos/networkextension/routemanager.mm +++ /dev/null @@ -1,511 +0,0 @@ -/* 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/. */ - -#import "routemanager.h" - -#import - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Private method in the network framework -extern "C" nw_interface_t nw_interface_create_with_index(int ifindex); - -@implementation RouteManager { - // The routing socket. - CFSocketRef m_socket; - int m_rtseq; - - NSObject* m_delegate; -} - -static void rawSockCallback(CFSocketRef s, CFSocketCallBackType cbType, - CFDataRef address, const void * data, void *info) { - RouteManager* monitor = (__bridge RouteManager*)info; - if (cbType == kCFSocketDataCallBack) { - [monitor rtDataCallback:(__bridge NSData*)data]; - } else { - NSLog(@"rawSockCallback: unexpected type %d", (int)cbType); - } -} - -static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa) { - size_t sa_len = ((const struct sockaddr*)sa)->sa_len; - if ((rtm->rtm_addrs & rtaddr) != 0) { - return; - } - if ((rtm->rtm_msglen + sa_len) > maxlen) { - return; - } - - memcpy((char*)rtm + rtm->rtm_msglen, sa, sa_len); - rtm->rtm_addrs |= rtaddr; - rtm->rtm_msglen += sa_len; - if (rtm->rtm_msglen % sizeof(uint32_t)) { - rtm->rtm_msglen += sizeof(uint32_t) - (rtm->rtm_msglen % sizeof(uint32_t)); - } -} - -static CFArrayRef rtmParseAddrList(const struct rt_msghdr* rtm, size_t hdrlen) { - constexpr int minlen = offsetof(struct sockaddr, sa_len) + sizeof(u_short); - const UInt8* data = (const UInt8*)rtm; - CFMutableArrayRef list = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks); - size_t offset = hdrlen; - - while ((offset + minlen) <= rtm->rtm_msglen) { - struct sockaddr* sa = (struct sockaddr*)(data + offset); - int paddedSize = sa->sa_len; - if (!paddedSize || (paddedSize % sizeof(uint32_t))) { - paddedSize += sizeof(uint32_t) - (paddedSize % sizeof(uint32_t)); - } - if ((offset + paddedSize) > rtm->rtm_msglen) { - break; - } - CFDataRef saData = CFDataCreate(kCFAllocatorDefault, (UInt8*)sa, paddedSize); - CFArrayAppendValue(list, saData); - offset += paddedSize; - } - return list; -} - -static const struct sockaddr* rtmLookupAddr(const struct rt_msghdr* rtm, int which, CFArrayRef addrs) { - if ((rtm->rtm_addrs & which) == 0) { - // Address is not included in this message - return NULL; - } - - // Figure out the index at which it should be present. - int index = 0; - for (int mask = 1; (mask & which) == 0; mask <<= 1) { - if (rtm->rtm_addrs & mask) { - index++; - } - } - - // Return the address in question. - if (index >= CFArrayGetCount(addrs)) { - return NULL; - } - CFDataRef data = (CFDataRef)CFArrayGetValueAtIndex(addrs, index); - return (struct sockaddr*)CFDataGetBytePtr(data); -} - -static NSString* rtmAddrString(const void *ptr) { - CFDataRef data = (CFDataRef)ptr; - const struct sockaddr* sa = (const struct sockaddr*)CFDataGetBytePtr(data); - if (sa->sa_len > CFDataGetLength(data)) { - return @"truncated"; - } - - if (sa->sa_family == AF_INET) { - const struct sockaddr_in* sin = (const struct sockaddr_in*)sa; - return [NSString stringWithUTF8String:inet_ntoa(sin->sin_addr)]; - } else if (sa->sa_family == AF_INET6) { - const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)sa; - char buf[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf)); - return [NSString stringWithUTF8String:buf]; - } else if (sa->sa_family == AF_LINK) { - const struct sockaddr_dl* sdl = (const struct sockaddr_dl*)sa; - return [NSString stringWithFormat:@"link#%d:%s", sdl->sdl_index, link_ntoa(sdl)]; - } else if (sa->sa_family == AF_UNSPEC) { - return @"unspec"; - } - - return [NSString stringWithFormat:@"unknown(af=%d)", sa->sa_family]; -} - -static void rtmLogRouteMsg(const struct rt_msghdr* rtm, CFArrayRef addrlist) { -#ifdef MZ_DEBUG - NSString* rtmType = nullptr; - switch (rtm->rtm_type) { - case RTM_ADD: - rtmType = @"RTM_ADD"; - break; - case RTM_DELETE: - rtmType = @"RTM_DELETE"; - break; - case RTM_CHANGE: - rtmType = @"RTM_CHANGE"; - break; - case RTM_GET: - rtmType = @"RTM_GET"; - break; - case RTM_IFINFO: - rtmType = @"RTM_IFINFO"; - break; - default: - rtmType = [NSString stringWithFormat:@"unknown(%d)", rtm->rtm_type]; - break; - } - - // Figure out the relevant interface name. - char ifname[IF_NAMESIZE] = "null"; - if (rtm->rtm_addrs & RTA_IFP) { - const struct sockaddr_dl* sdl = (const struct sockaddr_dl*)rtmLookupAddr(rtm, RTA_IFP, addrlist); - if (sdl && sdl->sdl_family == AF_LINK) { - if_indextoname(sdl->sdl_index, ifname); - } - } else if (rtm->rtm_index) { - if_indextoname(rtm->rtm_index, ifname); - } - - NSMutableString* details = [NSMutableString stringWithCapacity:0]; - // Log relevant updates to the routing table. - if (addrlist && CFArrayGetCount(addrlist)) { - [details appendFormat:@" addrs(%x)", rtm->rtm_addrs]; - for (int i = 0; i < CFArrayGetCount(addrlist); i++) { - [details appendString:@" "]; - [details appendString:rtmAddrString(CFArrayGetValueAtIndex(addrlist, i))]; - } - } - - NSLog(@"route %@ %s:%@", rtmType, ifname, details); -#endif -} - -// Compare memory against zero. -static int memcmpzero(const void* data, size_t len) { - const uint8_t* ptr = static_cast(data); - while (len--) { - if (*ptr++) return 1; - } - return 0; -} - -- (id)init { - self = [super init]; - NSLog(@"route manager created"); - - m_rtseq = 0; - int sockfd = socket(PF_ROUTE, SOCK_RAW, 0); - if (sockfd < 0) { - NSLog(@"failed to create routing socket: %s", strerror(errno)); - } - CFSocketContext ctx = { .info = (__bridge void *)self }; - m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, sockfd, kCFSocketDataCallBack, - rawSockCallback, &ctx); - - // Create a source and attach it to the main run loop. - CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopDefaultMode); - - return self; -} - -- (void)dealloc { - NSLog(@"route manager destroyed"); - - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - -#if !__has_feature(objc_arc) - [super dealloc]; -#endif -} - -- (void)startWithDelegate:(NSObject*)delegate { - m_delegate = delegate; - - // Grab the default routes at startup. - NSLog(@"Fetching default routes"); - [self rtmFetchRoutes:AF_INET]; - [self rtmFetchRoutes:AF_INET6]; -} - -- (void)rtDataCallback:(NSData*)data { -#ifndef RTMSG_NEXT -# define RTMSG_NEXT(_rtm_) \ - (struct rt_msghdr*)((char*)(_rtm_) + (_rtm_)->rtm_msglen) -#endif - - const char* buf = (const char*)data.bytes; - const struct rt_msghdr* rtm = (const struct rt_msghdr*)(buf); - const struct rt_msghdr* end = (const struct rt_msghdr*)(buf + data.length); - while (rtm < end) { - // Ensure the message fits within the buffer - if (RTMSG_NEXT(rtm) > end) { - NSLog(@"Routing message overflowed with length: %d", rtm->rtm_msglen); - break; - } - - CFArrayRef addrs = NULL; - switch (rtm->rtm_type) { - case RTM_ADD: - addrs = rtmParseAddrList(rtm, sizeof(struct rt_msghdr)); - [self handleRtmUpdate:rtm withAddrList:addrs]; - break; - case RTM_DELETE: - addrs = rtmParseAddrList(rtm, sizeof(struct rt_msghdr)); - [self handleRtmDelete:rtm withAddrList:addrs]; - break; - case RTM_CHANGE: - addrs = rtmParseAddrList(rtm, sizeof(struct rt_msghdr)); - [self handleRtmUpdate:rtm withAddrList:addrs]; - break; - case RTM_GET: - addrs = rtmParseAddrList(rtm, sizeof(struct rt_msghdr)); - [self handleRtmUpdate:rtm withAddrList:addrs]; - break; - case RTM_IFINFO: - //message.remove(0, sizeof(struct if_msghdr)); - //handleIfaceInfo((struct if_msghdr*)rtm, message); - break; - default: - break; - } - - if (addrs) { - CFRelease(addrs); - } - - rtm = RTMSG_NEXT(rtm); - } -} - -- (void) handleRtmUpdate:(const struct rt_msghdr*)rtm - withAddrList:(CFArrayRef)addrlist { - int ifindex = rtm->rtm_index; - - // We expect all useful routes to contain a destination, netmask and gateway. - if (!(rtm->rtm_addrs & RTA_DST) || !(rtm->rtm_addrs & RTA_GATEWAY) || - !(rtm->rtm_addrs & RTA_NETMASK) || (CFArrayGetCount(addrlist) < 3)) { - return; - } - // Ignore interface-scoped routes, we want to find the default route to the - // internet in the global scope. - if (rtm->rtm_flags & RTF_IFSCOPE) { - return; - } - // Ignore route changes that we caused, or routes on the tunnel interface. - //if (rtm->rtm_index == m_ifindex) { - // return; - //} - if ((rtm->rtm_pid == getpid()) && (rtm->rtm_type != RTM_GET)) { - return; - } - - // Log the relevant routing messages. - rtmLogRouteMsg(rtm, addrlist); - - // Special case: If RTA_IFP is set, then we should get the interface index - // from the address list instead of rtm_index. - if (rtmLookupAddr(rtm, RTA_IFP, addrlist)) { - const struct sockaddr_dl* sdl = (const struct sockaddr_dl*)rtmLookupAddr(rtm, RTA_IFP, addrlist); - if (sdl && sdl->sdl_family == AF_LINK) { - ifindex = sdl->sdl_index; - } - } - - // Check for a default route, which should have a netmask of zero. - const struct sockaddr* mask = rtmLookupAddr(rtm, RTA_NETMASK, addrlist); - if (mask->sa_family == AF_INET) { - struct sockaddr_in sin; - //Q_ASSERT(mask->sa_len <= sizeof(sin)); - memset(&sin, 0, sizeof(sin)); - memcpy(&sin, mask, mask->sa_len); - if (memcmpzero(&sin.sin_addr, sizeof(sin.sin_addr)) != 0) { - return; - } - } else if (mask->sa_family == AF_INET6) { - struct sockaddr_in6 sin6; - //Q_ASSERT(mask->sa_len <= sizeof(sin6)); - memset(&sin6, 0, sizeof(sin6)); - memcpy(&sin6, mask, mask->sa_len); - if (memcmpzero(&sin6.sin6_addr, sizeof(sin6.sin6_addr)) != 0) { - return; - } - } else if (mask->sa_family != AF_UNSPEC) { - // The default route sometimes sets a netmask of AF_UNSPEC. - return; - } - - // Notify the delegates about the default route update. - const struct sockaddr* gateway = rtmLookupAddr(rtm, RTA_GATEWAY, addrlist); - const struct sockaddr* dst = rtmLookupAddr(rtm, RTA_DST, addrlist); - nw_interface_t iface = nw_interface_create_with_index(ifindex); - if (m_delegate) { - NSData* gwData = [NSData dataWithBytes:gateway - length:gateway->sa_len]; - [m_delegate defaultRouteChanged:dst->sa_family - viaInterface:iface - withGateway:gwData]; - } -} - -- (void) handleRtmDelete:(const struct rt_msghdr*)rtm - withAddrList:(CFArrayRef)addrlist { - - // Ignore routing changes on the tunnel interface. - //if (rtm->rtm_index == m_ifindex) { - // return; - //} - - // We expect all useful routes to contain a destination, netmask and gateway. - if (!(rtm->rtm_addrs & RTA_DST) || !(rtm->rtm_addrs & RTA_GATEWAY) || - !(rtm->rtm_addrs & RTA_NETMASK) || (CFArrayGetCount(addrlist) < 3)) { - return; - } - // Ignore interface-scoped routes, we want to find the default route to the - // internet in the global scope. - if (rtm->rtm_flags & RTF_IFSCOPE) { - return; - } - - // Log the relevant routing messages. - rtmLogRouteMsg(rtm, addrlist); - - // Check for a default route, which should have a netmask of zero. - const struct sockaddr* mask = rtmLookupAddr(rtm, RTA_NETMASK, addrlist); - if (mask->sa_family == AF_INET) { - struct sockaddr_in sin; - //Q_ASSERT(mask->sa_len <= sizeof(sin)); - memset(&sin, 0, sizeof(sin)); - memcpy(&sin, mask, mask->sa_len); - if (memcmpzero(&sin.sin_addr, sizeof(sin.sin_addr)) != 0) { - return; - } - } else if (mask->sa_family == AF_INET6) { - struct sockaddr_in6 sin6; - //Q_ASSERT(mask->sa_len <= sizeof(sin6)); - memset(&sin6, 0, sizeof(sin6)); - memcpy(&sin6, mask, mask->sa_len); - if (memcmpzero(&sin6.sin6_addr, sizeof(sin6.sin6_addr)) != 0) { - return; - } - } else if (mask->sa_family != AF_UNSPEC) { - // We have sometimes seen the default route reported with AF_UNSPEC. - return; - } - - // Delete exclusion routes. - const struct sockaddr* dst = rtmLookupAddr(rtm, RTA_DST, addrlist); - NSLog(@"Lost default route"); - if (m_delegate) { - [m_delegate defaultRouteChanged:dst->sa_family - viaInterface:nil - withGateway:nil]; - } -} - -- (void)rtmFetchRoutes:(int)family { - int mib[] = { CTL_NET, PF_ROUTE, 0, family, NET_RT_DUMP, 0 }; - int miblen = sizeof(mib)/sizeof(int); - size_t bufsize = 0; - - // Get the size of the routing table. - if (sysctl(mib, miblen, nullptr, &bufsize, nullptr, 0) < 0) { - NSLog(@"Failed to get routing table size: %s", strerror(errno)); - return; - } - // Add a little extra in case of a race condition. - bufsize += 4096; - - // Fetch a copy of the routing table from the kernel. - char* buffer = (char*)malloc(bufsize); - if (sysctl(mib, miblen, buffer, &bufsize, nullptr, 0) < 0) { - NSLog(@"Failed to feetch routing table: %s", strerror(errno)); - free(buffer); - return; - } - - [self rtDataCallback:[NSData dataWithBytesNoCopy:buffer - length:bufsize - freeWhenDone:true]]; -} - -- (void) rtmSendRoute:(int)action - toDestination:(const struct sockaddr*)dst - withPrefix:(unsigned int)plen - viaInterface:(nw_interface_t)interface - withGateway:(NSData*)gateway - andFlags:(int)flags { - constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + - sizeof(struct sockaddr_in6) * 2 + - sizeof(struct sockaddr_storage); - char buf[rtm_max_size] = {0}; - struct rt_msghdr* rtm = reinterpret_cast(buf); - - rtm->rtm_msglen = sizeof(struct rt_msghdr); - rtm->rtm_version = RTM_VERSION; - rtm->rtm_type = action; - rtm->rtm_index = nw_interface_get_index(interface); - rtm->rtm_flags = flags | RTF_STATIC | RTF_UP; - rtm->rtm_addrs = 0; - rtm->rtm_pid = 0; - rtm->rtm_seq = m_rtseq++; - rtm->rtm_errno = 0; - rtm->rtm_inits = 0; - memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx)); - - // Append RTA_DST - rtmAppendAddr(rtm, rtm_max_size, RTA_DST, dst); - - // Append RTA_GATEWAY - if (gateway != nullptr) { - const struct sockaddr* gw = (const struct sockaddr*)gateway.bytes; - if ((gw->sa_family == AF_INET) || (gw->sa_family == AF_INET6)) { - rtm->rtm_flags |= RTF_GATEWAY; - } - rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, gw); - } else if (action != RTM_DELETE) { - const char* ifname = nw_interface_get_name(interface); - struct sockaddr_dl datalink; - memset(&datalink, 0, sizeof(datalink)); - datalink.sdl_family = AF_LINK; - datalink.sdl_len = offsetof(struct sockaddr_dl, sdl_data) + strlen(ifname); - datalink.sdl_index = nw_interface_get_index(interface); - datalink.sdl_type = IFT_OTHER; - datalink.sdl_nlen = strlen(ifname); - datalink.sdl_alen = 0; - datalink.sdl_slen = 0; - memcpy(&datalink.sdl_data, ifname, datalink.sdl_nlen); - rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, (struct sockaddr*)&datalink); - } - - // Append RTA_NETMASK - if (dst->sa_family == AF_INET6) { - struct sockaddr_in6 mask; - memset(&mask, 0, sizeof(mask)); - mask.sin6_family = AF_INET6; - mask.sin6_len = sizeof(mask); - memset(&mask.sin6_addr.s6_addr, 0xff, plen / 8); - if (plen % 8) { - mask.sin6_addr.s6_addr[plen / 8] = 0xFF ^ (0xFF >> (plen % 8)); - } - rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); - } else if (dst->sa_family == AF_INET) { - struct sockaddr_in mask; - memset(&mask, 0, sizeof(mask)); - mask.sin_family = AF_INET; - mask.sin_len = sizeof(struct sockaddr_in); - mask.sin_addr.s_addr = 0xffffffff; - if (plen < 32) { - mask.sin_addr.s_addr ^= htonl(0xffffffff >> plen); - } - rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); - } - - // Send the routing message into the kernel. - int len = write(CFSocketGetNative(m_socket), rtm, rtm->rtm_msglen); - if (len == rtm->rtm_msglen) { - return; - } - if ((action == RTM_ADD) && (errno == EEXIST)) { - return; - } - if ((action == RTM_DELETE) && (errno == ESRCH)) { - return; - } - NSLog(@"Failed to send route to kernel: %s", strerror(errno)); -} - -@end diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index cc4ad07b5a..7bed7f7ac5 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -10,7 +10,10 @@ #include #include +#include +#include #include +#include #include #include #include @@ -43,6 +46,10 @@ @implementation WireguardTunnel { struct timespec m_lastHandshake; struct timespec m_handshakeTimeout; + // The routing socket + int m_rtseq; + int m_rtsock; + // Not used for anything, we just hold them for status generation. nw_endpoint_t m_ipv4gateway; nw_endpoint_t m_ipv6gateway; @@ -70,6 +77,8 @@ - (id)init { set_logging_function(wgLog); m_tunfd = -1; + m_rtseq = 0; + m_rtsock = -1; m_wireguard = nil; m_dispatch = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); @@ -115,6 +124,13 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options return; } + // Create a routing socket too. + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); + if (m_rtsock < 0) { + [self shutdownTunnel:@"routing socket creation failed" withErrno:errno]; + return; + } + // Connect to the utun control kernel service. struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; int err = ioctl(m_tunfd, CTLIOCGINFO, &info); @@ -176,6 +192,16 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options return; } + // Configure routes into the tunnel interface. + // Note that the default route should set RTF_IFSCOPE so that we + // leave the real default route untouched. + for (RoutePrefix* prefix in options.routes) { + [self rtmSendRoute:RTM_ADD + toDestination:prefix.destination + withPrefix:prefix.prefixLength + andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; + } + #ifdef MZ_DEBUG char *addrstr = nw_endpoint_copy_address_string(options.serverIpv4Addr); NSLog(@"wireguard peer: %s port=%d", addrstr, nw_endpoint_get_port(options.serverIpv4Addr)); @@ -523,6 +549,11 @@ - (void)shutdownTunnel:(NSError*)error { m_tunfd = -1; } + if (m_rtsock >= 0) { + close(m_rtsock); + m_rtsock = -1; + } + if (m_wireguard) { tunnel_free(m_wireguard); m_wireguard = nil; @@ -602,4 +633,100 @@ - (void)setMtu:(NSUInteger)mtu { } } + +static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa) { + size_t sa_len = ((const struct sockaddr*)sa)->sa_len; + if ((rtm->rtm_addrs & rtaddr) != 0) { + return; + } + if ((rtm->rtm_msglen + sa_len) > maxlen) { + return; + } + + memcpy((char*)rtm + rtm->rtm_msglen, sa, sa_len); + rtm->rtm_addrs |= rtaddr; + rtm->rtm_msglen += sa_len; + if (rtm->rtm_msglen % sizeof(uint32_t)) { + rtm->rtm_msglen += sizeof(uint32_t) - (rtm->rtm_msglen % sizeof(uint32_t)); + } +} + +- (void)rtmSendRoute:(int)action + toDestination:(const struct sockaddr*)dest + withPrefix:(NSUInteger)plen + andFlags:(int)flags { + constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) + + sizeof(struct sockaddr_in6) * 2 + + sizeof(struct sockaddr_dl); + char buf[rtm_max_size] = {0}; + struct rt_msghdr* rtm = (struct rt_msghdr*)buf; + + rtm->rtm_msglen = sizeof(struct rt_msghdr); + rtm->rtm_version = RTM_VERSION; + rtm->rtm_type = action; + rtm->rtm_index = nw_interface_get_index(self.virtualInterface); + rtm->rtm_flags = flags | RTF_STATIC | RTF_UP; + rtm->rtm_addrs = 0; + rtm->rtm_pid = 0; + rtm->rtm_seq = m_rtseq++; + rtm->rtm_errno = 0; + rtm->rtm_inits = 0; + memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx)); + + // Append RTA_DST + rtmAppendAddr(rtm, rtm_max_size, RTA_DST, dest); + + // Append RTA_GATEWAY - unless deleting the route. + if (action != RTM_DELETE) { + const char* ifname = nw_interface_get_name(self.virtualInterface); + struct sockaddr_dl datalink; + memset(&datalink, 0, sizeof(datalink)); + datalink.sdl_family = AF_LINK; + datalink.sdl_len = offsetof(struct sockaddr_dl, sdl_data) + strlen(ifname); + datalink.sdl_index = nw_interface_get_index(self.virtualInterface); + datalink.sdl_type = IFT_OTHER; + datalink.sdl_nlen = strlen(ifname); + datalink.sdl_alen = 0; + datalink.sdl_slen = 0; + memcpy(datalink.sdl_data, ifname, datalink.sdl_nlen); + rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, (struct sockaddr*)&datalink); + } + + // Append RTM_NETMASK + if (dest->sa_family == AF_INET6) { + struct sockaddr_in6 mask; + memset(&mask, 0, sizeof(mask)); + mask.sin6_family = AF_INET6; + mask.sin6_len = sizeof(mask); + memset(&mask.sin6_addr.s6_addr, 0xff, plen / 8); + if (plen % 8) { + mask.sin6_addr.s6_addr[plen / 8] = 0xff ^ (0xff >> (plen % 8)); + } + rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); + } else if (dest->sa_family == AF_INET) { + struct sockaddr_in mask; + memset(&mask, 0, sizeof(mask)); + mask.sin_family = AF_INET; + mask.sin_len = sizeof(mask); + mask.sin_addr.s_addr = 0xffffffff; + if (plen < 32) { + mask.sin_addr.s_addr ^= htonl(0xffffffff >> plen); + } + rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); + } + + // Send the routing message into the kernel. + int len = write(m_rtsock, rtm, rtm->rtm_msglen); + if (len == rtm->rtm_msglen) { + return; + } + if ((action == RTM_ADD) && (errno == EEXIST)) { + return; + } + if ((action == RTM_DELETE) && (errno == ESRCH)) { + return; + } + NSLog(@"Failed to send route to kernel: %s", strerror(errno)); +} + @end From 75646c3f4e6db7f70d7bf4670ee15935c84033f8 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 28 Apr 2026 11:42:40 -0700 Subject: [PATCH 22/31] Fix a rebase fail in IOSController::checkStatus() --- src/platforms/ios/ioscontroller.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platforms/ios/ioscontroller.mm b/src/platforms/ios/ioscontroller.mm index 63f0c31474..e42d8c9117 100644 --- a/src/platforms/ios/ioscontroller.mm +++ b/src/platforms/ios/ioscontroller.mm @@ -223,7 +223,7 @@ void IOSController::deleteOSTunnelConfig() { [impl deleteOSTunnelConfig]; } -void IOSController::checkStatus(QObject* receiver) { +void IOSController::checkStatus() { logger.debug() << "Checking status"; if (m_checkingStatus) { @@ -273,6 +273,8 @@ st.m_ipv4Address = QHostAddress(QString::fromNSString(deviceIpv4Address)); st.m_rxBytes = rxBytes; st.m_txBytes = txBytes; + + emit statusUpdated(st); }]; } From 80930281d5abb7c4d7d6a0e2ccb1ea9b39d48e15 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 28 Apr 2026 11:55:12 -0700 Subject: [PATCH 23/31] Fix ConnectionHealth compile fail on MZ_MOBILE --- src/connectionhealth.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/connectionhealth.cpp b/src/connectionhealth.cpp index 941fe979ee..047b27ca45 100644 --- a/src/connectionhealth.cpp +++ b/src/connectionhealth.cpp @@ -259,14 +259,14 @@ void ConnectionHealth::applicationStateChanged(Qt::ApplicationState state) { #else switch (state) { case Qt::ApplicationState::ApplicationActive: - if (!m_suspended) return; - - m_suspended = false; - logger.debug() << "Resuming connection check from suspension"; - ConnectionStatus st; - st.m_ipv4Address = m_deviceAddress; - st.m_ipv6Address = m_currentGateway - startActive(st); + if (m_suspended) { + m_suspended = false; + logger.debug() << "Resuming connection check from suspension"; + ControllerStatus st; + st.m_ipv4Address = m_deviceAddress; + st.m_ipv4Gateway = m_currentGateway; + startActive(st); + } break; case Qt::ApplicationState::ApplicationSuspended: From 4a9cae62df038ae8a11b1847a96d995f2c222a46 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 30 Apr 2026 13:33:29 -0700 Subject: [PATCH 24/31] Refactoring: Split wireguard tunnel and peer into separate classes --- macos/networkextension/CMakeLists.txt | 4 + macos/networkextension/interfaceconfig.h | 2 +- macos/networkextension/utils.h | 22 ++ macos/networkextension/utils.mm | 82 ++++ macos/networkextension/wireguardpeer.h | 38 ++ macos/networkextension/wireguardpeer.mm | 281 ++++++++++++++ macos/networkextension/wireguardtunnel.h | 5 +- macos/networkextension/wireguardtunnel.mm | 442 +++++----------------- 8 files changed, 533 insertions(+), 343 deletions(-) create mode 100644 macos/networkextension/utils.h create mode 100644 macos/networkextension/utils.mm create mode 100644 macos/networkextension/wireguardpeer.h create mode 100644 macos/networkextension/wireguardpeer.mm diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index 43be5845ad..bdb0b4d198 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -71,6 +71,10 @@ target_sources(networkextension PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.h ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.mm ${CMAKE_CURRENT_SOURCE_DIR}/main.mm + ${CMAKE_CURRENT_SOURCE_DIR}/utils.h + ${CMAKE_CURRENT_SOURCE_DIR}/utils.mm + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardpeer.h + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardpeer.mm ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.h ${CMAKE_CURRENT_SOURCE_DIR}/wireguardstatus.mm ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.h diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h index d5f6f4692d..0d536b0bd9 100644 --- a/macos/networkextension/interfaceconfig.h +++ b/macos/networkextension/interfaceconfig.h @@ -7,7 +7,7 @@ @interface RoutePrefix : NSObject -+ (id)parseRoute:(NSString *)dict; ++ (id)parseRoute:(NSString *)routeString; @property (readonly, getter=getDestination) const struct sockaddr* destination; @property (readonly) NSUInteger prefixLength; diff --git a/macos/networkextension/utils.h b/macos/networkextension/utils.h new file mode 100644 index 0000000000..56627b31aa --- /dev/null +++ b/macos/networkextension/utils.h @@ -0,0 +1,22 @@ +/* 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/. */ + +#import +#import +#import + +#ifndef UTILS_H + +typedef enum { + kVPNSuccess = 0, + kVPNErrInvalidConfig = 1, + kVPNErrTunnelNotRunning = 2, +} VPNErrorType; + +nw_endpoint_t convertEndpoint(NWEndpoint* ep); +NSUInteger getWorkerCount(); +NSError* vpnProviderError(VPNErrorType err, NSString* msg); +NSError* vpnPosixError(int code, NSString* msg); + +#endif // UTILS_H diff --git a/macos/networkextension/utils.mm b/macos/networkextension/utils.mm new file mode 100644 index 0000000000..f85a7e5ab5 --- /dev/null +++ b/macos/networkextension/utils.mm @@ -0,0 +1,82 @@ +/* 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/. */ + +#import "utils.h" + +#include +#include + +// Aim to allocate roughly half the total core count as workers. +NSUInteger getWorkerCount() { + constexpr const int min_workers = 2; + + int mib[] = { CTL_HW, HW_NCPU }; + int num_cores = min_workers * 2; + size_t len = sizeof(num_cores); + if (sysctl(mib, sizeof(mib)/sizeof(int), &num_cores, &len, nullptr, 0) < 0) { + return min_workers; + } + + int num_workers = num_cores / 2; + return (num_workers < min_workers) ? min_workers : num_workers; +} + +nw_endpoint_t convertEndpoint(NWEndpoint* old) { + if (old == nil) { + return nil; + } else if ([old isKindOfClass:[NWBonjourServiceEndpoint class]]) { + NWBonjourServiceEndpoint* service = (NWBonjourServiceEndpoint*)old; + return nw_endpoint_create_bonjour_service([service.name UTF8String], + [service.type UTF8String], + [service.domain UTF8String]); + } else if (![old isKindOfClass:[NWHostEndpoint class]]) { + // Some endpoint type we don't support. + return nil; + } + NWHostEndpoint* host = (NWHostEndpoint*)old; + + // If possible, try to convert it into an address endpoint. + int port = host.port.intValue; + if ([host.hostname containsString:@":"]) { + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_port = htons(port); + if (inet_pton(AF_INET6, host.hostname.UTF8String, &sin6.sin6_addr.s6_addr)) { + return nw_endpoint_create_address((struct sockaddr*)&sin6); + } + } else { + struct sockaddr_in sin; + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_port = htons(port); + sin.sin_addr.s_addr = inet_addr(host.hostname.UTF8String); + if (sin.sin_addr.s_addr != INADDR_NONE) { + return nw_endpoint_create_address((struct sockaddr*)&sin); + } + } + + return nw_endpoint_create_host(host.hostname.UTF8String, host.port.UTF8String); +} + +NSError* vpnProviderError(VPNErrorType err, NSString* description) { + return [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] + code:(NSUInteger)err + userInfo:@{NSLocalizedDescriptionKey: description}]; +} + +NSError* vpnPosixError(int code, NSString* desc) { + NSString* msg; + if (desc) { + msg = [NSString stringWithFormat:@"%@: %s", desc, strerror(code)]; + } else { + msg = [NSString stringWithFormat:@"%s", strerror(code)]; + } + + return [NSError errorWithDomain:NSPOSIXErrorDomain + code:code + userInfo:@{NSLocalizedDescriptionKey: msg}]; +} diff --git a/macos/networkextension/wireguardpeer.h b/macos/networkextension/wireguardpeer.h new file mode 100644 index 0000000000..a1c1526309 --- /dev/null +++ b/macos/networkextension/wireguardpeer.h @@ -0,0 +1,38 @@ +/* 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/. */ + +#import +#import +#import + +#import "interfaceconfig.h" +#import "wireguardstatus.h" + +constexpr const int WG_PACKET_OVERHEAD = 32; +constexpr const int WG_PACKET_ALIGN = 16; +constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; +constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; + +@interface WireguardPeer : NSObject + +- (id) initWithOptions:(InterfaceConfig*) options + andTunnel:(int)fd; + +- (void) startWithOptions:(InterfaceConfig*) options + completionHandler:(void (^)(NSError *error)) completionHandler; + +- (void) stopWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)()) completionHandler; + +- (void) cancelWithError:(NSError*)error; + +- (void) renegotiate:(void (^)(NSError *error)) completionHandler; + +- (void) sendPacket:(int)protocol + withBytes:(const void*)data + length:(size_t)len; + +@property (strong, readonly, getter=getStatus) WireguardStatus* status; +@property (strong) nw_connection_t connection; +@end diff --git a/macos/networkextension/wireguardpeer.mm b/macos/networkextension/wireguardpeer.mm new file mode 100644 index 0000000000..9bd377ad24 --- /dev/null +++ b/macos/networkextension/wireguardpeer.mm @@ -0,0 +1,281 @@ +/* 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/. */ + +#import "wireguardpeer.h" + +#include +#include +#include +#include + +#include "utils.h" + +extern "C" { +#include "wireguard_ffi.h" +}; + +constexpr const int64_t PEER_WORKQUEUE_TIMEOUT = 30; +constexpr const size_t PEER_DATAGRAM_BUFSIZE = 4096; + +@implementation WireguardPeer { + struct wireguard_tunnel* m_wireguard; + CFRunLoopTimerRef m_timer; + struct timespec m_lastHandshake; + struct timespec m_handshakeTimeout; + + int m_socket; + int m_tunfd; + dispatch_queue_t m_dispatch; + dispatch_group_t m_workqueue; + + // The completion handler to run on initial handshake or timeout. + void (^m_completionHandler)(NSError *error); +} + +static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { + WireguardPeer* peer = (__bridge WireguardPeer*)info; + [peer handleTimer]; +} + +- (id) initWithOptions:(InterfaceConfig*) options + andTunnel:(int)fd { + self = [super init]; + self->m_tunfd = fd; + self->m_dispatch = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (m_socket < 0) { + return nil; + } + + uint32_t index; + getentropy(&index, sizeof(index)); + self->m_wireguard = new_tunnel(options.privateKey.UTF8String, + options.serverPublicKey.UTF8String, + nil, // Preshared key + 300, // Keepalive period + index % (1U << 24)); + if (!self->m_wireguard) { + return nil; + } + + return self; +} + +- (void) dealloc { + if (m_socket >= 0) { + close(m_socket); + } + if (m_wireguard) { + tunnel_free(m_wireguard); + } + +#if !__has_feature(objc_arc) + [super dealloc]; +#endif +} + +- (void) startWithOptions:(InterfaceConfig*) options + completionHandler:(void (^)(NSError *error)) completionHandler { +#ifdef MZ_DEBUG + char *addrstr = nw_endpoint_copy_address_string(options.serverIpv4Addr); + NSLog(@"wireguard peer: %s port=%d", addrstr, nw_endpoint_get_port(options.serverIpv4Addr)); + free(addrstr); +#endif + + const struct sockaddr* dest = nw_endpoint_get_address(options.serverIpv4Addr); + if (connect(m_socket, dest, dest->sa_len) < 0) { + completionHandler(vpnPosixError(errno, @"socket connect failed")); + return; + } + + // Start the timer. + CFRunLoopTimerContext timerContext = { .info = (__bridge void *)self }; + m_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 0.1, 0, 0, + wgTimerCallback, &timerContext); + CFRunLoopAddTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + + // Force an initial handshake. + [self renegotiate:completionHandler]; + + // Start inbound decryption workers. + m_workqueue = dispatch_group_create(); + [self startInboundWorker]; +} + +- (void) startInboundWorker { + dispatch_group_async(m_workqueue, m_dispatch, ^(void){ + uint8_t ciphertext[PEER_DATAGRAM_BUFSIZE]; + uint8_t plaintext[PEER_DATAGRAM_BUFSIZE]; + + while (true) { + int length = read(m_socket, ciphertext, sizeof(ciphertext)); + if (length == 0) { + // Socket has closed. + NSLog(@"socket closed"); + return; + } + if (length < 0) { + // Socket error occurred. + NSLog(@"socket error: %s", strerror(errno)); + if (errno == EINTR) continue; + return; + } + + // Decrypt the wireguard packet. + struct wireguard_result result; + result = wireguard_read(m_wireguard, ciphertext, length, plaintext, length); + [self handleWireguard:result withBuffer:plaintext]; + + // After processing a handshake response, update the lastHandshake time + // if it looks and smells like the handshake was successful. + if (ciphertext[0] == 0x02) { + struct stats wgStats = wireguard_stats(m_wireguard); + if (wgStats.time_since_last_handshake < 0) { + memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); + } else if (wgStats.time_since_last_handshake < 5) { + clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); + memset(&m_handshakeTimeout, 0, sizeof(m_handshakeTimeout)); + + // The conneciton is now up. + if (m_completionHandler) { + m_completionHandler(nil); + m_completionHandler = nil; + } + } + } + } + }); +} + +- (void) stopWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)()) completionHandler { + [self cancelWithError:vpnPosixError(ECANCELED, @"wireguard peer stopped")]; + completionHandler(); +} + +- (void) cancelWithError:(NSError*)error { + if (m_workqueue) { + shutdown(m_socket, SHUT_RDWR); + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, PEER_WORKQUEUE_TIMEOUT * 1000000000); + dispatch_group_wait(m_workqueue, delay); + m_workqueue = nil; + } + + if (m_timer) { + CFRunLoopRemoveTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + CFRelease(m_timer); + m_timer = nil; + } + + if (m_completionHandler) { + m_completionHandler(error); + m_completionHandler = nil; + } +} + +- (void) renegotiate:(void (^)(NSError *error)) completionHandler { + NSLog(@"wireguard renegotiate"); + uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; + + struct wireguard_result result; + result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + + // Set a timeout for the handshake to finish. + clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); + m_handshakeTimeout.tv_sec += WG_MAX_HANDSHAKE_TIMEOUT; + m_completionHandler = completionHandler; + + [self handleWireguard:result withBuffer:handshake]; +} + +- (void) sendPacket:(int)protocol + withBytes:(const void*)data + length:(size_t)length { + uint8_t ciphertext[length + WG_PACKET_OVERHEAD]; + struct wireguard_result result; + result = wireguard_write(m_wireguard, (const uint8_t*)data, length, + ciphertext, sizeof(ciphertext)); + [self handleWireguard:result withBuffer:ciphertext]; +} + +- (void) handleTimer { + // Check for a handshake timeout. + if (m_handshakeTimeout.tv_sec && m_handshakeTimeout.tv_nsec) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (now.tv_sec > m_handshakeTimeout.tv_sec) { + [self cancelWithError:vpnPosixError(ETIMEDOUT, @"handshake timeout")]; + return; + } else if (now.tv_sec < m_handshakeTimeout.tv_sec) { + // It has not timed out. + } else if (now.tv_nsec > m_handshakeTimeout.tv_nsec) { + [self cancelWithError:vpnPosixError(ETIMEDOUT, @"handshake timeout")]; + return; + } + } + + uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; + struct wireguard_result result; + result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + [self handleWireguard:result withBuffer:handshake]; +} + +- (void) handleWireguard:(struct wireguard_result)result + withBuffer:(const uint8_t*)data { + uint32_t header = htonl(AF_INET); + ssize_t err; + switch (result.op) { + case WIREGUARD_DONE: + break; + + case WRITE_TO_NETWORK: + err = send(m_socket, data, result.size, MSG_DONTWAIT); + break; + + case WIREGUARD_ERROR: + NSLog(@"peer error: %zu", result.size); + break; + + case WRITE_TO_TUNNEL_IPV6: + header = htonl(AF_INET6); + [[fallthrough]]; + case WRITE_TO_TUNNEL_IPV4: + struct iovec iov[2] = { + {.iov_base = &header, .iov_len = sizeof(header)}, + {.iov_base = (void *)data, .iov_len = result.size}, + }; + const struct msghdr msg = { + .msg_iov = iov, + .msg_iovlen = 2, + }; + ssize_t err = sendmsg(m_tunfd, &msg, MSG_DONTWAIT); + break; + } +} + +- (WireguardStatus*)getStatus { + WireguardStatus* result = [WireguardStatus new]; + + // Get the handshake time from the timestamp of the last received handshake + // response packet, this will yeild better precision than the stats we get + // from the boringtun API. + if (m_lastHandshake.tv_sec || m_lastHandshake.tv_nsec) { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + NSTimeInterval diff = (m_lastHandshake.tv_sec - now.tv_sec); + diff += (m_lastHandshake.tv_nsec - now.tv_nsec) / (1000000000.0); + result.lastHandshake = [NSDate dateWithTimeIntervalSinceNow:diff]; + } + + // Fetch the rest of the stats directly from boringtun. + struct stats wgStats = wireguard_stats(m_wireguard); + result.txBytes = wgStats.tx_bytes; + result.rxBytes = wgStats.rx_bytes; + result.estimatedLoss = wgStats.estimated_loss; + result.estimatedRtt = wgStats.estimated_rtt; + return result; +} + +@end diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h index 18394877d9..0444f36e15 100644 --- a/macos/networkextension/wireguardtunnel.h +++ b/macos/networkextension/wireguardtunnel.h @@ -7,6 +7,7 @@ #import #import "interfaceconfig.h" +#import "wireguardpeer.h" #import "wireguardstatus.h" @interface WireguardTunnel : NSObject @@ -22,11 +23,9 @@ - (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; @property (nonatomic) NSUInteger mtu; +@property (strong) WireguardPeer* peer; @property (strong, readonly, getter=getStatus) WireguardStatus* status; @property (strong) nw_endpoint_t ipv4address; @property (strong) nw_endpoint_t ipv6address; - -@property (strong) nw_connection_t connection; @property (strong) nw_interface_t virtualInterface; - @end diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 7bed7f7ac5..68c123bfad 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -28,6 +28,8 @@ #include #include +#import "utils.h" + extern "C" { #include "wireguard_ffi.h" }; @@ -36,16 +38,10 @@ extern "C" nw_interface_t nw_interface_create_with_index_and_name(int ifindex, const char *ifname); @implementation WireguardTunnel { - struct wireguard_tunnel* m_wireguard; - int m_tunfd; - CFRunLoopTimerRef m_timer; dispatch_queue_t m_dispatch; dispatch_group_t m_workqueue; - struct timespec m_lastHandshake; - struct timespec m_handshakeTimeout; - // The routing socket int m_rtseq; int m_rtsock; @@ -53,25 +49,14 @@ @implementation WireguardTunnel { // Not used for anything, we just hold them for status generation. nw_endpoint_t m_ipv4gateway; nw_endpoint_t m_ipv6gateway; - - // The completion handler to run on initial handshake or timeout. - void (^m_completionHandler)(NSError *error); } -constexpr const int WG_PACKET_OVERHEAD = 32; -constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; -constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; constexpr const int64_t WG_WORKQUEUE_TIMEOUT = 30; static void wgLog(const char* msg) { NSLog(@"wg: %s", msg); } -static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { - WireguardTunnel* tunnel = (__bridge WireguardTunnel*)info; - [tunnel handleTimer]; -} - - (id)init { self = [super init]; set_logging_function(wgLog); @@ -79,64 +64,19 @@ - (id)init { m_tunfd = -1; m_rtseq = 0; m_rtsock = -1; - m_wireguard = nil; m_dispatch = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); - _mtu = IPV6_MMTU; - return self; -} - -static NSError* errorFromErrno(int code, NSString* desc) { - if (!desc) { - desc = @"error occurred"; - } - NSString* msg = [NSString stringWithFormat:@"%@: %s", desc, strerror(code)]; - NSLog(@"%@", msg); - - return [NSError errorWithDomain:NSPOSIXErrorDomain - code:code - userInfo:@{NSLocalizedDescriptionKey: msg}]; -} - -// Aim to allocate roughly half the total core count as workers. -static int getWorkerCount() { - int mib[] = { CTL_HW, HW_NCPU }; - int count = 4; - size_t length = sizeof(count); - sysctl(mib, sizeof(mib)/sizeof(int), &count, &length, nullptr, 0); - if (count < 2) { - return 1; - } else { - return count / 2; - } -} - -- (void) startTunnelWithOptions:(InterfaceConfig *)options - completionHandler:(void (^)(NSError *error)) completionHandler { - m_completionHandler = completionHandler; - m_workqueue = dispatch_group_create(); - m_ipv4gateway = options.serverIpv4Gateway; - m_ipv6gateway = options.serverIpv6Gateway; - m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (m_tunfd < 0) { - [self shutdownTunnel:@"tunnel creation failed" withErrno:errno]; - return; - } - - // Create a routing socket too. - m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); - if (m_rtsock < 0) { - [self shutdownTunnel:@"routing socket creation failed" withErrno:errno]; - return; + return nil; } // Connect to the utun control kernel service. struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; int err = ioctl(m_tunfd, CTLIOCGINFO, &info); if (err < 0) { - [self shutdownTunnel:@"kernel utun lookup failed" withErrno:errno]; - return; + close(m_tunfd); + return nil; } struct sockaddr_ctl addr = {}; addr.sc_len = sizeof(addr); @@ -146,8 +86,8 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options addr.sc_unit = 0; err = connect(m_tunfd, (struct sockaddr*)&addr, sizeof(addr)); if (err < 0) { - [self shutdownTunnel:@"kernel utun connect failed" withErrno:errno]; - return; + close(m_tunfd); + return nil; } // Get the tunnel device's name. @@ -156,39 +96,48 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, &ifnamesize); if (err < 0) { - [self shutdownTunnel:@"utun name loookup failed" withErrno:errno]; - return; + close(m_tunfd); + return nil; } int ifindex = if_nametoindex(ifr.ifr_name); self.virtualInterface = nw_interface_create_with_index_and_name(ifindex, ifr.ifr_name); - // Assign addresses - if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { - [self shutdownTunnel:err]; - return; - } - if (NSError *err = [self setTunnelAddress:options.deviceIpv6Addr]) { - [self shutdownTunnel:err]; - return; + // Set a base MTU, it will get updated later. + ifr.ifr_mtu = IPV6_MMTU; + if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) == 0) { + _mtu = IPV6_MMTU; + } else if (ioctl(m_tunfd, SIOCGIFMTU, &ifr) == 0) { + _mtu = ifr.ifr_mtu; + } else { + _mtu = IPV6_MMTU; } - // Set a base MTU, it will get updated later. - ifr.ifr_mtu = self.mtu; - if (ioctl(m_tunfd, SIOCSIFMTU, &ifr) != 0) { - [self shutdownTunnel:@"failed to set mtu" withErrno:errno]; + // Start outbound encryption workers. + m_workqueue = dispatch_group_create(); + [self startOutboundWorker]; + + return self; +} + +- (void) startTunnelWithOptions:(InterfaceConfig *)options + completionHandler:(void (^)(NSError *error)) completionHandler { + m_ipv4gateway = options.serverIpv4Gateway; + m_ipv6gateway = options.serverIpv6Gateway; + + // Create a routing socket too. + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); + if (m_rtsock < 0) { + completionHandler(vpnPosixError(errno, @"routing socket creation failed")); return; } - // Bring the device up. - err = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); - if (err != 0) { - [self shutdownTunnel:@"failed to get interface flags" withErrno:errno]; + // Assign addresses + if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { + completionHandler(err); return; } - ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); - err = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); - if (err != 0) { - [self shutdownTunnel:@"failed to set device up" withErrno:errno]; + if (NSError *err = [self setTunnelAddress:options.deviceIpv6Addr]) { + completionHandler(err); return; } @@ -202,84 +151,66 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; } -#ifdef MZ_DEBUG - char *addrstr = nw_endpoint_copy_address_string(options.serverIpv4Addr); - NSLog(@"wireguard peer: %s port=%d", addrstr, nw_endpoint_get_port(options.serverIpv4Addr)); - free(addrstr); -#endif - - nw_parameters_t params = nw_parameters_create_secure_udp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); - self.connection = nw_connection_create(options.serverIpv4Addr, params); - nw_connection_set_queue(self.connection, m_dispatch); + // Configure the peer + self.peer = [[WireguardPeer alloc] initWithOptions:options andTunnel:m_tunfd]; + [self.peer startWithOptions:options completionHandler:^(NSError* error){ + if (error) { + completionHandler(error); + return; + } - // (Re)-create the Wireguard tunnel structure. - if (m_wireguard) { - tunnel_free(m_wireguard); - } + // Update the MTU once the socket is open + //self.mtu = nw_connection_get_maximum_datagram_size(self.peer.connection) - WG_PACKET_OVERHEAD; - uint32_t index; - getentropy(&index, sizeof(index)); - m_wireguard = new_tunnel(options.privateKey.UTF8String, - options.serverPublicKey.UTF8String, - nil, // Preshared key - 300, // Keepalive period - index % (1U << 24)); - - m_completionHandler = completionHandler; - nw_connection_set_state_changed_handler(self.connection, - ^(nw_connection_state_t state, nw_error_t err) { - if (err) { - CFErrorRef cfError = nw_error_copy_cf_error(err); - NSLog(@"vpn socket error: %@", (__bridge NSError*)cfError); - [self cancelTunnelWithError:(__bridge NSError*)cfError]; - CFRelease(cfError); - } else if (state == nw_connection_state_cancelled || state == nw_connection_state_failed) { - NSLog(@"vpn socket closed"); - } else if (state != nw_connection_state_ready) { - NSLog(@"vpn socket state %d", state); - } else { - NSLog(@"vpn socket opened"); - // Update the MTU once the socket is open - self.mtu = nw_connection_get_maximum_datagram_size(self.connection) - WG_PACKET_OVERHEAD; - - // Start the timer. - CFRunLoopTimerContext timerContext = { .info = (__bridge void *)self }; - m_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 0.1, 0, 0, - wgTimerCallback, &timerContext); - CFRunLoopAddTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); - - // Force an initial handshake and begin packet processing. - [self renegotiate]; - [self handleInbound]; - - // Start outbound encryption workers. - for (int i = 0; i < getWorkerCount(); i++) { - [self startOutboundWorker]; - } + // Bring the device up, if not already done. + struct ifreq ifr; + socklen_t ifnamesize = sizeof(ifr.ifr_name); + int result = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, + ifr.ifr_name, &ifnamesize); + if (result < 0) { + completionHandler(vpnPosixError(errno, @"failed to get interface name")); + return; } - }); - nw_connection_start(self.connection); + result = ioctl(m_tunfd, SIOCGIFFLAGS, &ifr); + if (result) { + completionHandler(vpnPosixError(errno, @"failed to get interface flags")); + return; + } + ifr.ifr_flags |= (IFF_UP | IFF_RUNNING); + result = ioctl(m_tunfd, SIOCSIFFLAGS, &ifr); + if (result != 0) { + completionHandler(vpnPosixError(errno, @"failed to set device up")); + return; + } + + completionHandler(nil); + }]; } - (void) stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)()) completionHandler { - m_completionHandler = ^(NSError *error){ + if (!self.peer) { + [self shutdownTunnel]; completionHandler(); - }; + } - [self shutdownTunnel:nil]; + [self.peer stopWithReason:reason + completionHandler:^(){ + [self shutdownTunnel]; + completionHandler(); + }]; } - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { if (nw_endpoint_get_type(endpoint) != nw_endpoint_type_address) { - return errorFromErrno(EINVAL, @"failed to set tunnel address"); + return vpnPosixError(EINVAL, @"failed to set tunnel address"); } const struct sockaddr *sa = nw_endpoint_get_address(endpoint); NSError *err = nil; int sock = socket(sa->sa_family, SOCK_DGRAM, IPPROTO_IP); if (sock < 0) { - return errorFromErrno(errno, @"failed to set tunnel address"); + return vpnPosixError(errno, @"failed to set tunnel address"); } if (sa->sa_family == AF_INET) { @@ -303,7 +234,7 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { bcast->sin_addr.s_addr = 0xffffffff; if (ioctl(sock, SIOCAIFADDR, &ifr) < 0) { - err = errorFromErrno(errno, @"failed to set tunnel address"); + err = vpnPosixError(errno, @"failed to set tunnel address"); } else { self.ipv4address = endpoint; } @@ -322,59 +253,22 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { memset(&mask->sin6_addr, 0xff, sizeof(struct in6_addr)); if (ioctl(sock, SIOCAIFADDR_IN6, &ifr6) < 0) { - err = errorFromErrno(errno, @"failed to set tunnel address"); + err = vpnPosixError(errno, @"failed to set tunnel address"); } else { self.ipv6address = endpoint; } } else { // We don't recognize this address type. - err = errorFromErrno(EAFNOSUPPORT, @"failed to set tunnel address"); + err = vpnPosixError(EAFNOSUPPORT, @"failed to set tunnel address"); } close(sock); return err; } -- (void) renegotiate { - NSLog(@"wireguard renegotiate"); - uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; - - struct wireguard_result result; - result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); - - // Set a timeout for the handshake to finish. - clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); - m_handshakeTimeout.tv_sec += WG_MAX_HANDSHAKE_TIMEOUT; - - [self handleWireguard:result withBuffer:handshake]; -} - -- (void) handleTimer { - // Check for a handshake timeout. - if (m_handshakeTimeout.tv_sec && m_handshakeTimeout.tv_nsec) { - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - if (now.tv_sec > m_handshakeTimeout.tv_sec) { - [self shutdownTunnel:@"handshake timeout" withErrno:ETIMEDOUT]; - return; - } else if (now.tv_sec < m_handshakeTimeout.tv_sec) { - // It has not timed out. - } else if (now.tv_nsec > m_handshakeTimeout.tv_nsec) { - [self shutdownTunnel:@"handshake timeout" withErrno:ETIMEDOUT]; - return; - } - } - - uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; - struct wireguard_result result; - result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); - [self handleWireguard:result withBuffer:handshake]; -} - - (void) startOutboundWorker { dispatch_group_async(m_workqueue, m_dispatch, ^(void){ size_t mtu = self.mtu; - uint8_t plaintext[mtu + 16]; - uint8_t ciphertext[mtu + WG_PACKET_OVERHEAD]; + uint8_t plaintext[mtu + WG_PACKET_ALIGN]; uint32_t header; struct iovec iov[2]; @@ -405,137 +299,27 @@ - (void) startOutboundWorker { // The wireguard protocol states that the encapsulated packet must first // be padded out to a multiple of 16 bytes in length, but boringtun does // no such padding during encryption. So let's do it manually ourself. - int tail = pktlen % 16; + int tail = pktlen % WG_PACKET_ALIGN; if (tail) { - memset(plaintext + pktlen, 0, 16 - tail); - pktlen += 16 - tail; + memset(plaintext + pktlen, 0, WG_PACKET_ALIGN - tail); + pktlen += WG_PACKET_ALIGN - tail; } - // Encrypt the wireguard packet. - struct wireguard_result result; - result = wireguard_write(m_wireguard, plaintext, pktlen, ciphertext, - sizeof(ciphertext)); - [self handleWireguard:result withBuffer:ciphertext]; - } - }); -} - -- (void) handleInbound { - nw_connection_receive_message(self.connection, - ^(dispatch_data_t data, nw_content_context_t ctx, bool done, nw_error_t err){ - if (err) { - CFErrorRef cfError = nw_error_copy_cf_error(err); - NSLog(@"recv error: %@", (__bridge NSError *)cfError); - CFRelease(cfError); - return; - } - - if (data == nil) { - // This may be nil if the message or stream is complete. - NSLog(@"recv empty"); - return; - } - - size_t length; - const void *ciphertext; - dispatch_data_t __unused map = dispatch_data_create_map(data, &ciphertext, &length); - uint8_t* plaintext = (uint8_t*)malloc(length); - - // Decrypt the wireguard packet. - struct wireguard_result result; - result = wireguard_read(m_wireguard, (const uint8_t*)ciphertext, length, - plaintext, length); - - // After processing a handshake response, update the lastHandshake time - // if it looks and smells like the handshake was successful. - if (*(const uint8_t*)ciphertext == 0x02) { - struct stats wgStats = wireguard_stats(m_wireguard); - if (wgStats.time_since_last_handshake < 0) { - memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); - } else if (wgStats.time_since_last_handshake < 5) { - clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); - - // Clear the handshake timeout - memset(&m_handshakeTimeout, 0, sizeof(m_handshakeTimeout)); - - // The conneciton is now up. - if (m_completionHandler) { - m_completionHandler(nil); - m_completionHandler = nil; - } + // If there is no peer to handle this packet, then drop it. + if (!self.peer) { + continue; } - } - [self handleWireguard:result withBuffer:plaintext]; - free(plaintext); - - // Keep going to receive more data. - [self handleInbound]; - }); -} - -- (void) handleWireguard:(struct wireguard_result)result - withBuffer:(const uint8_t*)data { - uint32_t header = htonl(AF_INET); - switch (result.op) { - case WIREGUARD_DONE: - break; - - case WRITE_TO_NETWORK:{ - dispatch_data_t dgram = dispatch_data_create(data, result.size, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_DEFAULT); - nw_connection_send(self.connection, dgram, - NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, true, - ^(nw_error_t error) { - if (error) { - CFErrorRef cfError = nw_error_copy_cf_error(error); - NSLog(@"wireguard send error: %@", (__bridge NSError*)cfError); - CFRelease(cfError); - } - }); + [self.peer sendPacket:htonl(header) + withBytes:plaintext + length:pktlen]; } - break; - - case WIREGUARD_ERROR: - NSLog(@"wireguard error: %zu", result.size); - break; - - case WRITE_TO_TUNNEL_IPV6: - header = htonl(AF_INET6); - [[fallthrough]]; - case WRITE_TO_TUNNEL_IPV4: - const struct iovec iov[2] = { - {.iov_base = &header, .iov_len = sizeof(header)}, - {.iov_base = (void *)data, .iov_len = result.size}, - }; - int err = writev(m_tunfd, iov, 2); - break; - } -} - -- (void)shutdownTunnel:(NSString*)desc withErrno:(int)code { - [self shutdownTunnel:errorFromErrno(code, desc)]; + }); } -- (void)shutdownTunnel:(NSError*)error { - if (error) { - NSLog(@"wireguard shutdown: %@", error); - } else { - NSLog(@"wireguard shutdown"); - } - if (self.connection) { - nw_connection_cancel(self.connection); - self.connection = nil; - } +- (void)shutdownTunnel { self.virtualInterface = nil; - if (m_timer) { - CFRunLoopRemoveTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); - CFRelease(m_timer); - m_timer = nil; - } - // Shutdown the tunnel workers. if (m_workqueue) { shutdown(m_tunfd, SHUT_RDWR); @@ -553,35 +337,22 @@ - (void)shutdownTunnel:(NSError*)error { close(m_rtsock); m_rtsock = -1; } - - if (m_wireguard) { - tunnel_free(m_wireguard); - m_wireguard = nil; - } - - if (m_completionHandler) { - m_completionHandler(error); - m_completionHandler = nil; - } } - (void)cancelTunnelWithError:(NSError*)error { - [self shutdownTunnel:error]; + if (self.peer) { + [self.peer cancelWithError:error]; + self.peer = nil; + } + + [self shutdownTunnel]; } - (WireguardStatus*)getStatus { - WireguardStatus* result = [WireguardStatus new]; - - // Get the handshake time from the timestamp of the last received handshake - // response packet, this will yeild better precision than the stats we get - // from the boringtun API. - if (m_lastHandshake.tv_sec || m_lastHandshake.tv_nsec) { - struct timespec now; - clock_gettime(CLOCK_MONOTONIC, &now); - NSTimeInterval diff = (m_lastHandshake.tv_sec - now.tv_sec); - diff += (m_lastHandshake.tv_nsec - now.tv_nsec) / (1000000000.0); - result.lastHandshake = [NSDate dateWithTimeIntervalSinceNow:diff]; + if (!self.peer) { + return [WireguardStatus new]; } + WireguardStatus* result = self.peer.status; // Get the interface addresses. if (self.ipv4address) { @@ -613,12 +384,6 @@ - (WireguardStatus*)getStatus { freeWhenDone:YES]; } - // Fetch the rest of the stats directly from boringtun. - struct stats wgStats = wireguard_stats(m_wireguard); - result.txBytes = wgStats.tx_bytes; - result.rxBytes = wgStats.rx_bytes; - result.estimatedLoss = wgStats.estimated_loss; - result.estimatedRtt = wgStats.estimated_rtt; return result; } @@ -633,7 +398,6 @@ - (void)setMtu:(NSUInteger)mtu { } } - static void rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen, int rtaddr, const void* sa) { size_t sa_len = ((const struct sockaddr*)sa)->sa_len; if ((rtm->rtm_addrs & rtaddr) != 0) { From 35920e96639a42f3a5c194ff6ce61f4cb09df756 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 12 May 2026 14:08:02 -0700 Subject: [PATCH 25/31] Move packet rx and tx into their own worker threads --- macos/networkextension/wireguardpeer.h | 8 +- macos/networkextension/wireguardpeer.mm | 133 ++++++++++++---------- macos/networkextension/wireguardtunnel.mm | 112 +++++++++--------- 3 files changed, 133 insertions(+), 120 deletions(-) diff --git a/macos/networkextension/wireguardpeer.h b/macos/networkextension/wireguardpeer.h index a1c1526309..e351e997a0 100644 --- a/macos/networkextension/wireguardpeer.h +++ b/macos/networkextension/wireguardpeer.h @@ -14,6 +14,8 @@ constexpr const int WG_PACKET_ALIGN = 16; constexpr const int WG_MAX_HANDSHAKE_SIZE = 148; constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; +extern "C" struct wireguard_tunnel; + @interface WireguardPeer : NSObject - (id) initWithOptions:(InterfaceConfig*) options @@ -29,10 +31,10 @@ constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; - (void) renegotiate:(void (^)(NSError *error)) completionHandler; -- (void) sendPacket:(int)protocol - withBytes:(const void*)data - length:(size_t)len; +- (void) writePacket:(int)protocol + withData:(NSData*)data; @property (strong, readonly, getter=getStatus) WireguardStatus* status; @property (strong) nw_connection_t connection; +@property struct wireguard_tunnel* wireguard; @end diff --git a/macos/networkextension/wireguardpeer.mm b/macos/networkextension/wireguardpeer.mm index 9bd377ad24..0a4f3385ab 100644 --- a/macos/networkextension/wireguardpeer.mm +++ b/macos/networkextension/wireguardpeer.mm @@ -19,15 +19,13 @@ constexpr const size_t PEER_DATAGRAM_BUFSIZE = 4096; @implementation WireguardPeer { - struct wireguard_tunnel* m_wireguard; CFRunLoopTimerRef m_timer; struct timespec m_lastHandshake; struct timespec m_handshakeTimeout; int m_socket; int m_tunfd; - dispatch_queue_t m_dispatch; - dispatch_group_t m_workqueue; + NSThread* m_worker; // The completion handler to run on initial handshake or timeout. void (^m_completionHandler)(NSError *error); @@ -42,7 +40,10 @@ - (id) initWithOptions:(InterfaceConfig*) options andTunnel:(int)fd { self = [super init]; self->m_tunfd = fd; - self->m_dispatch = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + + // Create the dispatch queue. + NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier]; + NSString* queueId = [NSString stringWithFormat:@"%@.wireguard", bundleId]; m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (m_socket < 0) { @@ -51,15 +52,19 @@ - (id) initWithOptions:(InterfaceConfig*) options uint32_t index; getentropy(&index, sizeof(index)); - self->m_wireguard = new_tunnel(options.privateKey.UTF8String, + self.wireguard = new_tunnel(options.privateKey.UTF8String, options.serverPublicKey.UTF8String, nil, // Preshared key 300, // Keepalive period index % (1U << 24)); - if (!self->m_wireguard) { + if (!self.wireguard) { return nil; } + // Give the socket its own worker. + self->m_worker = [[NSThread alloc] initWithTarget:self + selector:@selector(socketWorker:) + object:nil]; return self; } @@ -67,8 +72,8 @@ - (void) dealloc { if (m_socket >= 0) { close(m_socket); } - if (m_wireguard) { - tunnel_free(m_wireguard); + if (self.wireguard) { + tunnel_free(self.wireguard); } #if !__has_feature(objc_arc) @@ -99,54 +104,58 @@ - (void) startWithOptions:(InterfaceConfig*) options // Force an initial handshake. [self renegotiate:completionHandler]; - // Start inbound decryption workers. - m_workqueue = dispatch_group_create(); - [self startInboundWorker]; + // Start the socket worker + [m_worker start]; } -- (void) startInboundWorker { - dispatch_group_async(m_workqueue, m_dispatch, ^(void){ - uint8_t ciphertext[PEER_DATAGRAM_BUFSIZE]; - uint8_t plaintext[PEER_DATAGRAM_BUFSIZE]; +- (void) socketWorker:(id)arg { + NSLog(@"socket worker started"); - while (true) { - int length = read(m_socket, ciphertext, sizeof(ciphertext)); - if (length == 0) { - // Socket has closed. - NSLog(@"socket closed"); - return; - } - if (length < 0) { - // Socket error occurred. - NSLog(@"socket error: %s", strerror(errno)); - if (errno == EINTR) continue; - return; - } + NSThread* thread = [NSThread currentThread]; + while (!thread.cancelled) { + NSMutableData* dgram = [NSMutableData dataWithLength:PEER_DATAGRAM_BUFSIZE]; + int rx = read(m_socket, dgram.mutableBytes, dgram.length); + if (rx == 0) { + // Socket has closed. + NSLog(@"socket closed"); + return; + } + if (rx < 0) { + // Socket error occurred. + if (errno == EINTR) continue; + NSLog(@"socket error: %s", strerror(errno)); + return; + } + dgram.length = rx; + uint8_t wgMsgtype = *(const uint8_t *)dgram.bytes; - // Decrypt the wireguard packet. - struct wireguard_result result; - result = wireguard_read(m_wireguard, ciphertext, length, plaintext, length); - [self handleWireguard:result withBuffer:plaintext]; - - // After processing a handshake response, update the lastHandshake time - // if it looks and smells like the handshake was successful. - if (ciphertext[0] == 0x02) { - struct stats wgStats = wireguard_stats(m_wireguard); - if (wgStats.time_since_last_handshake < 0) { - memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); - } else if (wgStats.time_since_last_handshake < 5) { - clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); - memset(&m_handshakeTimeout, 0, sizeof(m_handshakeTimeout)); - - // The conneciton is now up. - if (m_completionHandler) { - m_completionHandler(nil); - m_completionHandler = nil; - } + uint8_t plaintext[PEER_DATAGRAM_BUFSIZE]; + struct wireguard_result result; + result = wireguard_read(self.wireguard, (const uint8_t *)dgram.bytes, + dgram.length, plaintext, PEER_DATAGRAM_BUFSIZE); + [self handleWireguard:result withBuffer:plaintext]; + + // After processing a handshake response, update the lastHandshake time + // if it looks and smells like the handshake was successful. + if (wgMsgtype == 0x02) { + struct stats wgStats = wireguard_stats(self.wireguard); + if (wgStats.time_since_last_handshake < 0) { + memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); + } else if (wgStats.time_since_last_handshake < 5) { + clock_gettime(CLOCK_MONOTONIC, &m_lastHandshake); + memset(&m_handshakeTimeout, 0, sizeof(m_handshakeTimeout)); + + // The conneciton is now up. + if (m_completionHandler) { + auto block = m_completionHandler; + m_completionHandler = nil; + + // We have to do this from the main thread or it all falls apart. + dispatch_async(dispatch_get_main_queue(), ^(){ block(nil); }); } } } - }); + } } - (void) stopWithReason:(NEProviderStopReason)reason @@ -156,12 +165,10 @@ - (void) stopWithReason:(NEProviderStopReason)reason } - (void) cancelWithError:(NSError*)error { - if (m_workqueue) { - shutdown(m_socket, SHUT_RDWR); - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, PEER_WORKQUEUE_TIMEOUT * 1000000000); - dispatch_group_wait(m_workqueue, delay); - m_workqueue = nil; + if (m_worker) { + [m_worker cancel]; } + shutdown(m_socket, SHUT_RDWR); if (m_timer) { CFRunLoopRemoveTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); @@ -180,7 +187,7 @@ - (void) renegotiate:(void (^)(NSError *error)) completionHandler { uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; - result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + result = wireguard_force_handshake(self.wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); // Set a timeout for the handshake to finish. clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); @@ -190,13 +197,13 @@ - (void) renegotiate:(void (^)(NSError *error)) completionHandler { [self handleWireguard:result withBuffer:handshake]; } -- (void) sendPacket:(int)protocol - withBytes:(const void*)data - length:(size_t)length { - uint8_t ciphertext[length + WG_PACKET_OVERHEAD]; +- (void) writePacket:(int)protocol + withData:(NSData*)data { + uint8_t ciphertext[data.length + WG_PACKET_OVERHEAD]; + const uint8_t* plaintext = (const uint8_t*)data.bytes; struct wireguard_result result; - result = wireguard_write(m_wireguard, (const uint8_t*)data, length, - ciphertext, sizeof(ciphertext)); + result = wireguard_write(self.wireguard, plaintext, data.length, + ciphertext, sizeof(ciphertext)); [self handleWireguard:result withBuffer:ciphertext]; } @@ -218,7 +225,7 @@ - (void) handleTimer { uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; - result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + result = wireguard_tick(self.wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); [self handleWireguard:result withBuffer:handshake]; } @@ -270,7 +277,7 @@ - (WireguardStatus*)getStatus { } // Fetch the rest of the stats directly from boringtun. - struct stats wgStats = wireguard_stats(m_wireguard); + struct stats wgStats = wireguard_stats(self.wireguard); result.txBytes = wgStats.tx_bytes; result.rxBytes = wgStats.rx_bytes; result.estimatedLoss = wgStats.estimated_loss; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 68c123bfad..f07634ae9a 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -39,8 +39,8 @@ @implementation WireguardTunnel { int m_tunfd; - dispatch_queue_t m_dispatch; - dispatch_group_t m_workqueue; + dispatch_semaphore_t m_semaphore; + NSThread * m_worker; // The routing socket int m_rtseq; @@ -51,7 +51,7 @@ @implementation WireguardTunnel { nw_endpoint_t m_ipv6gateway; } -constexpr const int64_t WG_WORKQUEUE_TIMEOUT = 30; +constexpr const int64_t WG_WORKQUEUE_TIMEOUT = 5LL * 1000000000LL; static void wgLog(const char* msg) { NSLog(@"wg: %s", msg); @@ -64,7 +64,6 @@ - (id)init { m_tunfd = -1; m_rtseq = 0; m_rtsock = -1; - m_dispatch = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (m_tunfd < 0) { @@ -113,8 +112,11 @@ - (id)init { } // Start outbound encryption workers. - m_workqueue = dispatch_group_create(); - [self startOutboundWorker]; + m_semaphore = dispatch_semaphore_create(0); + m_worker = [[NSThread alloc] initWithTarget:self + selector:@selector(tunnelWorker:) + object:nil]; + [m_worker start]; return self; } @@ -154,6 +156,7 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options // Configure the peer self.peer = [[WireguardPeer alloc] initWithOptions:options andTunnel:m_tunfd]; [self.peer startWithOptions:options completionHandler:^(NSError* error){ + NSLog(@"handshake completed"); if (error) { completionHandler(error); return; @@ -265,67 +268,68 @@ - (NSError*)setTunnelAddress:(nw_endpoint_t)endpoint { return err; } -- (void) startOutboundWorker { - dispatch_group_async(m_workqueue, m_dispatch, ^(void){ - size_t mtu = self.mtu; - uint8_t plaintext[mtu + WG_PACKET_ALIGN]; - uint32_t header; +- (void) tunnelWorker:(id)arg { + size_t mtu = self.mtu; + uint8_t plaintext[mtu + WG_PACKET_ALIGN]; + uint32_t header; + + NSThread* thread = [NSThread currentThread]; + while (!thread.cancelled) { + NSMutableData* buffer = [NSMutableData dataWithLength:mtu + WG_PACKET_ALIGN]; struct iovec iov[2]; iov[0].iov_base = &header; iov[0].iov_len = sizeof(header); - iov[1].iov_base = plaintext; + iov[1].iov_base = buffer.mutableBytes; iov[1].iov_len = mtu; - while (true) { - int rx = readv(m_tunfd, iov, 2); - if (rx == 0) { - // Socket has closed. - NSLog(@"utun closed"); - return; - } - if (rx < 0) { - // Socket error occurred. - NSLog(@"utun error: %s", strerror(errno)); - if (errno == EINTR) continue; - return; - } - int pktlen = rx - sizeof(header); - if ((pktlen < 0) || (pktlen > mtu)) { - continue; - } - - // I think there is a small bug in boringtun to do with message padding. - // The wireguard protocol states that the encapsulated packet must first - // be padded out to a multiple of 16 bytes in length, but boringtun does - // no such padding during encryption. So let's do it manually ourself. - int tail = pktlen % WG_PACKET_ALIGN; - if (tail) { - memset(plaintext + pktlen, 0, WG_PACKET_ALIGN - tail); - pktlen += WG_PACKET_ALIGN - tail; - } - - // If there is no peer to handle this packet, then drop it. - if (!self.peer) { - continue; - } - - [self.peer sendPacket:htonl(header) - withBytes:plaintext - length:pktlen]; + int rx = readv(m_tunfd, iov, 2); + if (rx == 0) { + // Socket has closed. + NSLog(@"utun closed"); + return; + } + if (rx < 0) { + // Socket error occurred. + NSLog(@"utun error: %s", strerror(errno)); + if (errno == EINTR) continue; + return; + } + if ((rx < sizeof(header)) || (rx > (mtu - sizeof(header)))) { + continue; + } + buffer.length = rx - sizeof(header); + + // I think there is a small bug in boringtun to do with message padding. + // The wireguard protocol states that the encapsulated packet must first + // be padded out to a multiple of 16 bytes in length, but boringtun does + // no such padding during encryption. So let's do it manually ourself. + int tail = buffer.length % WG_PACKET_ALIGN; + if (tail) { + buffer.length += WG_PACKET_ALIGN - tail; } - }); + + // If there is no peer to handle this packet, then drop it. + if (!self.peer) { + continue; + } + + [self.peer writePacket:htonl(header) + withData:buffer]; + } } - (void)shutdownTunnel { self.virtualInterface = nil; - // Shutdown the tunnel workers. - if (m_workqueue) { + // Shutdown the tunnel worker. + if (m_worker) { shutdown(m_tunfd, SHUT_RDWR); - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, WG_WORKQUEUE_TIMEOUT * 1000000000); - dispatch_group_wait(m_workqueue, delay); - m_workqueue = nil; + [m_worker cancel]; + m_worker = nil; + + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, WG_WORKQUEUE_TIMEOUT); + dispatch_semaphore_wait(m_semaphore, delay); } if (m_tunfd >= 0) { From ad7c6847f7445f0274a311e4d4538ac96dd38b36 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Wed, 13 May 2026 13:37:41 -0700 Subject: [PATCH 26/31] Configure the utun packet backlog size --- macos/networkextension/wireguardtunnel.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index f07634ae9a..094d2e019f 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -71,7 +71,7 @@ - (id)init { } // Connect to the utun control kernel service. - struct ctl_info info = {.ctl_name = "com.apple.net.utun_control"}; + struct ctl_info info = {.ctl_name = UTUN_CONTROL_NAME}; int err = ioctl(m_tunfd, CTLIOCGINFO, &info); if (err < 0) { close(m_tunfd); @@ -89,6 +89,11 @@ - (id)init { return nil; } + // Allow packets to queue up in the kernel when under load. + uint32_t utun_max_backlog = 64; + setsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_MAX_PENDING_PACKETS, + &utun_max_backlog, sizeof(utun_max_backlog)); + // Get the tunnel device's name. struct ifreq ifr; socklen_t ifnamesize = sizeof(ifr.ifr_name); From 51ba36646d7c5ffa38f414461272862b0c396bf4 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 14 May 2026 07:35:16 -0700 Subject: [PATCH 27/31] Move peer route setup into WireguardPeer class --- macos/networkextension/wireguardpeer.h | 6 ++- macos/networkextension/wireguardpeer.mm | 57 ++++++++++++++--------- macos/networkextension/wireguardtunnel.h | 4 ++ macos/networkextension/wireguardtunnel.mm | 42 +++++++++-------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/macos/networkextension/wireguardpeer.h b/macos/networkextension/wireguardpeer.h index e351e997a0..de28ababe0 100644 --- a/macos/networkextension/wireguardpeer.h +++ b/macos/networkextension/wireguardpeer.h @@ -16,10 +16,12 @@ constexpr const int WG_MAX_HANDSHAKE_TIMEOUT = 15; extern "C" struct wireguard_tunnel; +@class WireguardTunnel; + @interface WireguardPeer : NSObject - (id) initWithOptions:(InterfaceConfig*) options - andTunnel:(int)fd; + andTunnel:(WireguardTunnel*) tunnel; - (void) startWithOptions:(InterfaceConfig*) options completionHandler:(void (^)(NSError *error)) completionHandler; @@ -35,6 +37,6 @@ extern "C" struct wireguard_tunnel; withData:(NSData*)data; @property (strong, readonly, getter=getStatus) WireguardStatus* status; +@property (weak, readonly) WireguardTunnel* tunnel; @property (strong) nw_connection_t connection; -@property struct wireguard_tunnel* wireguard; @end diff --git a/macos/networkextension/wireguardpeer.mm b/macos/networkextension/wireguardpeer.mm index 0a4f3385ab..f349f28c5a 100644 --- a/macos/networkextension/wireguardpeer.mm +++ b/macos/networkextension/wireguardpeer.mm @@ -10,6 +10,7 @@ #include #include "utils.h" +#include "wireguardtunnel.h" extern "C" { #include "wireguard_ffi.h" @@ -19,6 +20,9 @@ constexpr const size_t PEER_DATAGRAM_BUFSIZE = 4096; @implementation WireguardPeer { + // The wireguard tunnel provided by boringtun. + struct wireguard_tunnel* m_wireguard; + CFRunLoopTimerRef m_timer; struct timespec m_lastHandshake; struct timespec m_handshakeTimeout; @@ -27,6 +31,9 @@ @implementation WireguardPeer { int m_tunfd; NSThread* m_worker; + // Routes owned by this peer. + NSArray* m_routes; + // The completion handler to run on initial handshake or timeout. void (^m_completionHandler)(NSError *error); } @@ -37,13 +44,10 @@ static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { } - (id) initWithOptions:(InterfaceConfig*) options - andTunnel:(int)fd { + andTunnel:(WireguardTunnel*) tunnel { self = [super init]; - self->m_tunfd = fd; - - // Create the dispatch queue. - NSString* bundleId = [[NSBundle mainBundle] bundleIdentifier]; - NSString* queueId = [NSString stringWithFormat:@"%@.wireguard", bundleId]; + self->m_tunfd = tunnel.tunfd; + self->_tunnel = tunnel; m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (m_socket < 0) { @@ -52,12 +56,12 @@ - (id) initWithOptions:(InterfaceConfig*) options uint32_t index; getentropy(&index, sizeof(index)); - self.wireguard = new_tunnel(options.privateKey.UTF8String, - options.serverPublicKey.UTF8String, - nil, // Preshared key - 300, // Keepalive period - index % (1U << 24)); - if (!self.wireguard) { + m_wireguard = new_tunnel(options.privateKey.UTF8String, + options.serverPublicKey.UTF8String, + nil, // Preshared key + 300, // Keepalive period + index % (1U << 24)); + if (!m_wireguard) { return nil; } @@ -72,8 +76,8 @@ - (void) dealloc { if (m_socket >= 0) { close(m_socket); } - if (self.wireguard) { - tunnel_free(self.wireguard); + if (m_wireguard) { + tunnel_free(m_wireguard); } #if !__has_feature(objc_arc) @@ -95,6 +99,12 @@ - (void) startWithOptions:(InterfaceConfig*) options return; } + // Configure routes into the tunnel interface. + m_routes = [NSArray arrayWithArray:options.routes]; + for (RoutePrefix* prefix in m_routes) { + [self.tunnel addRoute:prefix]; + } + // Start the timer. CFRunLoopTimerContext timerContext = { .info = (__bridge void *)self }; m_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 0.1, 0, 0, @@ -131,14 +141,14 @@ - (void) socketWorker:(id)arg { uint8_t plaintext[PEER_DATAGRAM_BUFSIZE]; struct wireguard_result result; - result = wireguard_read(self.wireguard, (const uint8_t *)dgram.bytes, + result = wireguard_read(m_wireguard, (const uint8_t *)dgram.bytes, dgram.length, plaintext, PEER_DATAGRAM_BUFSIZE); [self handleWireguard:result withBuffer:plaintext]; // After processing a handshake response, update the lastHandshake time // if it looks and smells like the handshake was successful. if (wgMsgtype == 0x02) { - struct stats wgStats = wireguard_stats(self.wireguard); + struct stats wgStats = wireguard_stats(m_wireguard); if (wgStats.time_since_last_handshake < 0) { memset(&m_lastHandshake, 0, sizeof(m_lastHandshake)); } else if (wgStats.time_since_last_handshake < 5) { @@ -170,6 +180,11 @@ - (void) cancelWithError:(NSError*)error { } shutdown(m_socket, SHUT_RDWR); + // Drop routes into the tunnel. + for (RoutePrefix* prefix in m_routes) { + [self.tunnel removeRoute:prefix]; + } + if (m_timer) { CFRunLoopRemoveTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); CFRelease(m_timer); @@ -187,7 +202,7 @@ - (void) renegotiate:(void (^)(NSError *error)) completionHandler { uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; - result = wireguard_force_handshake(self.wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + result = wireguard_force_handshake(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); // Set a timeout for the handshake to finish. clock_gettime(CLOCK_MONOTONIC, &m_handshakeTimeout); @@ -202,8 +217,8 @@ - (void) writePacket:(int)protocol uint8_t ciphertext[data.length + WG_PACKET_OVERHEAD]; const uint8_t* plaintext = (const uint8_t*)data.bytes; struct wireguard_result result; - result = wireguard_write(self.wireguard, plaintext, data.length, - ciphertext, sizeof(ciphertext)); + result = wireguard_write(m_wireguard, plaintext, data.length, + ciphertext, sizeof(ciphertext)); [self handleWireguard:result withBuffer:ciphertext]; } @@ -225,7 +240,7 @@ - (void) handleTimer { uint8_t handshake[WG_MAX_HANDSHAKE_SIZE]; struct wireguard_result result; - result = wireguard_tick(self.wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); + result = wireguard_tick(m_wireguard, handshake, WG_MAX_HANDSHAKE_SIZE); [self handleWireguard:result withBuffer:handshake]; } @@ -277,7 +292,7 @@ - (WireguardStatus*)getStatus { } // Fetch the rest of the stats directly from boringtun. - struct stats wgStats = wireguard_stats(self.wireguard); + struct stats wgStats = wireguard_stats(m_wireguard); result.txBytes = wgStats.tx_bytes; result.rxBytes = wgStats.rx_bytes; result.estimatedLoss = wgStats.estimated_loss; diff --git a/macos/networkextension/wireguardtunnel.h b/macos/networkextension/wireguardtunnel.h index 0444f36e15..51f76485db 100644 --- a/macos/networkextension/wireguardtunnel.h +++ b/macos/networkextension/wireguardtunnel.h @@ -22,8 +22,12 @@ - (NSError*) setTunnelAddress:(nw_endpoint_t)endpoint; +- (void) addRoute:(RoutePrefix*)prefix; +- (void) removeRoute:(RoutePrefix*)prefix; + @property (nonatomic) NSUInteger mtu; @property (strong) WireguardPeer* peer; +@property (readonly, getter=getTunfd) int tunfd; @property (strong, readonly, getter=getStatus) WireguardStatus* status; @property (strong) nw_endpoint_t ipv4address; @property (strong) nw_endpoint_t ipv6address; diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index 094d2e019f..b725b960e1 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -63,8 +63,7 @@ - (id)init { m_tunfd = -1; m_rtseq = 0; - m_rtsock = -1; - + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (m_tunfd < 0) { return nil; @@ -131,13 +130,6 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options m_ipv4gateway = options.serverIpv4Gateway; m_ipv6gateway = options.serverIpv6Gateway; - // Create a routing socket too. - m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); - if (m_rtsock < 0) { - completionHandler(vpnPosixError(errno, @"routing socket creation failed")); - return; - } - // Assign addresses if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { completionHandler(err); @@ -148,18 +140,8 @@ - (void) startTunnelWithOptions:(InterfaceConfig *)options return; } - // Configure routes into the tunnel interface. - // Note that the default route should set RTF_IFSCOPE so that we - // leave the real default route untouched. - for (RoutePrefix* prefix in options.routes) { - [self rtmSendRoute:RTM_ADD - toDestination:prefix.destination - withPrefix:prefix.prefixLength - andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; - } - // Configure the peer - self.peer = [[WireguardPeer alloc] initWithOptions:options andTunnel:m_tunfd]; + self.peer = [[WireguardPeer alloc] initWithOptions:options andTunnel:self]; [self.peer startWithOptions:options completionHandler:^(NSError* error){ NSLog(@"handshake completed"); if (error) { @@ -357,6 +339,10 @@ - (void)cancelTunnelWithError:(NSError*)error { [self shutdownTunnel]; } +- (int)getTunfd { + return m_tunfd; +} + - (WireguardStatus*)getStatus { if (!self.peer) { return [WireguardStatus new]; @@ -502,4 +488,20 @@ - (void)rtmSendRoute:(int)action NSLog(@"Failed to send route to kernel: %s", strerror(errno)); } +- (void) addRoute:(RoutePrefix*)prefix { + // Note that the default route should set RTF_IFSCOPE so that we + // leave the real default route untouched. + [self rtmSendRoute:RTM_ADD + toDestination:prefix.destination + withPrefix:prefix.prefixLength + andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; +} + +- (void) removeRoute:(RoutePrefix*)prefix { + [self rtmSendRoute:RTM_DELETE + toDestination:prefix.destination + withPrefix:prefix.prefixLength + andFlags:(prefix.prefixLength == 0) ? RTF_IFSCOPE : 0]; +} + @end From c1699b5f9817356f774c93397582913ba0e2bb0e Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 14 May 2026 07:47:40 -0700 Subject: [PATCH 28/31] Set RTF_HOST for host routes instead of using a netmask --- macos/networkextension/wireguardtunnel.mm | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/macos/networkextension/wireguardtunnel.mm b/macos/networkextension/wireguardtunnel.mm index b725b960e1..15fab994a9 100644 --- a/macos/networkextension/wireguardtunnel.mm +++ b/macos/networkextension/wireguardtunnel.mm @@ -457,11 +457,13 @@ - (void)rtmSendRoute:(int)action memset(&mask, 0, sizeof(mask)); mask.sin6_family = AF_INET6; mask.sin6_len = sizeof(mask); - memset(&mask.sin6_addr.s6_addr, 0xff, plen / 8); - if (plen % 8) { - mask.sin6_addr.s6_addr[plen / 8] = 0xff ^ (0xff >> (plen % 8)); + if (plen < 128) { + memset(&mask.sin6_addr.s6_addr, 0xff, plen / 8); + mask.sin6_addr.s6_addr[plen / 8] = ~(0xff >> (plen % 8)); + rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); + } else { + rtm->rtm_flags |= RTF_HOST; } - rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); } else if (dest->sa_family == AF_INET) { struct sockaddr_in mask; memset(&mask, 0, sizeof(mask)); @@ -469,9 +471,11 @@ - (void)rtmSendRoute:(int)action mask.sin_len = sizeof(mask); mask.sin_addr.s_addr = 0xffffffff; if (plen < 32) { - mask.sin_addr.s_addr ^= htonl(0xffffffff >> plen); + mask.sin_addr.s_addr = ~htonl(0xffffffff >> plen); + rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); + } else { + rtm->rtm_flags |= RTF_HOST; } - rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &mask); } // Send the routing message into the kernel. From c1e04e945a0af9bd2a62747b2e5152778a4808c7 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 14 May 2026 08:40:44 -0700 Subject: [PATCH 29/31] Turns out we can perform bypassing by binding the interface now --- macos/networkextension/CMakeLists.txt | 4 - .../VPNSplitTunnelProvider.mm | 49 +-- macos/networkextension/bypasstcpflow.h | 16 - macos/networkextension/bypasstcpflow.mm | 155 --------- macos/networkextension/bypassudpflow.h | 15 - macos/networkextension/bypassudpflow.mm | 294 ------------------ 6 files changed, 2 insertions(+), 531 deletions(-) delete mode 100644 macos/networkextension/bypasstcpflow.h delete mode 100644 macos/networkextension/bypasstcpflow.mm delete mode 100644 macos/networkextension/bypassudpflow.h delete mode 100644 macos/networkextension/bypassudpflow.mm diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index bdb0b4d198..dc313a0581 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -64,10 +64,6 @@ add_custom_command(TARGET networkextension POST_BUILD target_link_libraries(networkextension PRIVATE ${FW_FOUNDATION}) target_link_libraries(networkextension PRIVATE ${FW_NW_EXTENSION}) target_sources(networkextension PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/bypasstcpflow.h - ${CMAKE_CURRENT_SOURCE_DIR}/bypasstcpflow.mm - ${CMAKE_CURRENT_SOURCE_DIR}/bypassudpflow.h - ${CMAKE_CURRENT_SOURCE_DIR}/bypassudpflow.mm ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.h ${CMAKE_CURRENT_SOURCE_DIR}/interfaceconfig.mm ${CMAKE_CURRENT_SOURCE_DIR}/main.mm diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index a2dab0b973..80c4a8c9bc 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -4,8 +4,6 @@ #import -#import "bypasstcpflow.h" -#import "bypassudpflow.h" #import "interfaceconfig.h" #import "wireguardtunnel.h" @@ -316,58 +314,15 @@ - (BOOL)handleNewFlow:(NEAppProxyFlow*) flow { } // Perform flow bypassing. + flow.networkInterface = self.wireguard.virtualInterface; if ([flow isKindOfClass:[NEAppProxyTCPFlow class]]) { - NEAppProxyTCPFlow* tcpFlow = (NEAppProxyTCPFlow*)flow; - nw_endpoint_t dest = nil; - if (@available(macOS 15, *)) { - dest = tcpFlow.remoteFlowEndpoint; - } else { - dest = [VPNSplitTunnelProvider convertEndpoint:tcpFlow.remoteEndpoint]; - } - - BypassTcpFlow* handler = [BypassTcpFlow createBypass:tcpFlow - toEndpoint:dest - withInterface:self.wireguard.virtualInterface]; - if (!handler) { - return NO; - } - - [handler startBypass:^(NSError* error){ - if (error) { - NSLog(@"flow closed with error: %@", error); - } - }]; - std::atomic_fetch_add(&m_handledTcpFlows, 1); - return YES; } else if ([flow isKindOfClass:[NEAppProxyUDPFlow class]]) { - NEAppProxyUDPFlow* udpFlow = (NEAppProxyUDPFlow*)flow; - nw_endpoint_t source; - if (@available(macOS 15, *)) { - source = udpFlow.localFlowEndpoint; - } else { - source = [VPNSplitTunnelProvider convertEndpoint:udpFlow.localEndpoint]; - } - - BypassUdpFlow* handler = [BypassUdpFlow createBypass:udpFlow - localEndpoint:source - withInterface:self.wireguard.virtualInterface]; - if (!handler) { - return NO; - } - - [handler startBypass:^(NSError* error){ - if (error) { - NSLog(@"flow closed with error: %@", error); - } - }]; - std::atomic_fetch_add(&m_handledUdpFlows, 1); - return YES; } else { std::atomic_fetch_add(&m_handledUnknown, 1); } - return NO; + return YES; } - (void)cancelProxyWithError:(NSError *)error { diff --git a/macos/networkextension/bypasstcpflow.h b/macos/networkextension/bypasstcpflow.h deleted file mode 100644 index fb6150f75a..0000000000 --- a/macos/networkextension/bypasstcpflow.h +++ /dev/null @@ -1,16 +0,0 @@ -/* 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/. */ - -@interface BypassTcpFlow : NSObject - -+ (id)createBypass:(NEAppProxyTCPFlow*)flow - toEndpoint:(nw_endpoint_t)endpoint - withInterface:(nw_interface_t)interface; - -- (void)startBypass:(void (^)(NSError* error))completionHandler; - -@property(strong) NEAppProxyTCPFlow* flow; -@property(strong) nw_connection_t connection; - -@end diff --git a/macos/networkextension/bypasstcpflow.mm b/macos/networkextension/bypasstcpflow.mm deleted file mode 100644 index 4d8a5cab6f..0000000000 --- a/macos/networkextension/bypasstcpflow.mm +++ /dev/null @@ -1,155 +0,0 @@ -/* 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/. */ - -#import - -#import "bypasstcpflow.h" - -@implementation BypassTcpFlow - -+ (id)createBypass:(NEAppProxyTCPFlow *)flow - toEndpoint:(nw_endpoint_t)endpoint - withInterface:(nw_interface_t)interface { - BypassTcpFlow* bypass = [BypassTcpFlow new]; - bypass.flow = flow; - - // Bind the flow to the default network interface route. - nw_parameters_t params = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, NW_PARAMETERS_DEFAULT_CONFIGURATION); - nw_parameters_require_interface(params, interface); - - bypass.connection = nw_connection_create(endpoint, params); - nw_connection_set_queue(bypass.connection, dispatch_get_main_queue()); - - return bypass; -} - -- (void)startBypass:(void (^)(NSError *error)) completionHandler { - nw_connection_set_state_changed_handler(self.connection, - ^(nw_connection_state_t state, nw_error_t err) { - if (err) { - CFErrorRef cfError = nw_error_copy_cf_error(err); - [self closeConnection:(__bridge NSError*)cfError completionHandler:completionHandler]; - CFRelease(cfError); - } else if (state == nw_connection_state_cancelled || state == nw_connection_state_failed) { - NSLog(@"bypass state closed"); - [self closeConnection:nil completionHandler:completionHandler]; - } else if (state != nw_connection_state_ready) { - NSLog(@"bypass state %d", state); - } else if (@available(macOS 15, *)) { - NSLog(@"bypass opening"); - [self.flow openWithLocalFlowEndpoint:nil - completionHandler:^(NSError* openError){ - if (openError) { - NSLog(@"bypass open error: %@", openError); - [self closeConnection:openError completionHandler:completionHandler]; - } else { - NSLog(@"bypass data begin"); - [self handleOutbound:completionHandler]; - [self handleInbound:completionHandler]; - } - }]; - } else { - NSLog(@"bypass opening (legacy)"); - [self.flow openWithLocalEndpoint:nil - completionHandler:^(NSError* openError){ - if (openError) { - NSLog(@"bypass open error: %@", openError); - [self closeConnection:openError completionHandler:completionHandler]; - } else { - NSLog(@"bypass data begin (legacy)"); - [self handleOutbound:completionHandler]; - [self handleInbound:completionHandler]; - } - }]; - } - }); - - nw_connection_start(self.connection); -} - -- (void)handleOutbound:(void (^)(NSError *error)) completionHandler { - [self.flow readDataWithCompletionHandler:^(NSData *data, NSError *error) { - if (error) { - [self closeConnection:error completionHandler:completionHandler]; - return; - } - - // If there was no data, try again. - if (!data) { - [self handleOutbound:completionHandler]; - return; - } - - // Outbound data flow terminated gracefully. - if (data.length == 0) { - [self closeConnection:nil completionHandler:completionHandler]; - return; - } - - // Forward the data out to the network - dispatch_data_t chunk = dispatch_data_create(data.bytes, data.length, - dispatch_get_main_queue(), - DISPATCH_DATA_DESTRUCTOR_DEFAULT); - nw_connection_send(self.connection, chunk, NW_CONNECTION_DEFAULT_MESSAGE_CONTEXT, - true, ^(nw_error_t sendError) { - if (sendError) { - CFErrorRef cfError = nw_error_copy_cf_error(sendError); - [self closeConnection:(__bridge NSError*)cfError completionHandler:completionHandler]; - CFRelease(cfError); - } else { - [self handleOutbound:completionHandler]; - } - }); - }]; -} - -- (void)handleInbound:(void (^)(NSError *)) completionHandler { - nw_connection_receive(self.connection, 1, UINT16_MAX, - ^(dispatch_data_t data, nw_content_context_t ctx, bool done, nw_error_t err){ - if (err) { - CFErrorRef cfError = nw_error_copy_cf_error(err); - [self closeConnection:(__bridge NSError *)cfError completionHandler:completionHandler]; - CFRelease(cfError); - return; - } - if (!data) { - [self closeConnection:nil completionHandler:completionHandler]; - return; - } - - // Forward the data to the app proxy flow. - const void *buffer; - size_t length; - dispatch_data_t __unused map = dispatch_data_create_map(data, &buffer, &length); - NSData* chunk = [NSData dataWithBytes:buffer length:length]; - [self.flow writeData:chunk withCompletionHandler:^(NSError* recvError){ - if (recvError) { - [self closeConnection:recvError completionHandler:completionHandler]; - } else if (done) { - [self closeConnection:nil completionHandler:completionHandler]; - } else { - [self handleInbound:completionHandler]; - } - }]; - }); -} - -- (void)closeConnection:(NSError *)error - completionHandler:(void (^)(NSError *)) completionHandler { - NSLog(@"bypass close"); - if(self.connection) { - nw_connection_cancel(self.connection); - self.connection = nil; - } - - if (self.flow) { - [self.flow closeReadWithError:error]; - [self.flow closeWriteWithError:error]; - self.flow = nil; - } - - completionHandler(error); -} - -@end diff --git a/macos/networkextension/bypassudpflow.h b/macos/networkextension/bypassudpflow.h deleted file mode 100644 index ab8a2f974b..0000000000 --- a/macos/networkextension/bypassudpflow.h +++ /dev/null @@ -1,15 +0,0 @@ -/* 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/. */ - -@interface BypassUdpFlow : NSObject - -+ (id)createBypass:(NEAppProxyUDPFlow*)flow - localEndpoint:(nw_endpoint_t)endpoint - withInterface:(nw_interface_t)interface; - -- (void)startBypass:(void (^)(NSError* error))completionHandler; - -@property(strong) NEAppProxyUDPFlow* flow; - -@end diff --git a/macos/networkextension/bypassudpflow.mm b/macos/networkextension/bypassudpflow.mm deleted file mode 100644 index 2df5f544cb..0000000000 --- a/macos/networkextension/bypassudpflow.mm +++ /dev/null @@ -1,294 +0,0 @@ -/* 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/. */ - -#import - -#include - -#import "bypassudpflow.h" - -@implementation BypassUdpFlow { - CFSocketRef m_socket; - CFRunLoopSourceRef m_source; -} - -static void udpSockCallback(CFSocketRef s, CFSocketCallBackType cbType, - CFDataRef address, const void * data, void *info) { - BypassUdpFlow* bypass = (__bridge BypassUdpFlow*)info; - if (cbType == kCFSocketDataCallBack) { - const struct sockaddr* sa = (const struct sockaddr*)CFDataGetBytePtr(address); - [bypass recvDatagram:(__bridge NSData*)data - fromEndpoint:nw_endpoint_create_address(sa)]; - } else { - NSLog(@"udpSockCallback: unexpected type %d", (int)cbType); - } -} - -+ (id)createBypass:(NEAppProxyUDPFlow *)flow - localEndpoint:(nw_endpoint_t)endpoint - withInterface:(nw_interface_t)interface { - // If the packet flow is already bound then there is nothing to do here. - if (flow.isBound) { - return nil; - } - - int family = AF_INET; - if (endpoint && (nw_endpoint_get_type(endpoint) == nw_endpoint_type_address)) { - family = nw_endpoint_get_address(endpoint)->sa_family; - } - - BypassUdpFlow* bypass = [BypassUdpFlow new]; - bypass.flow = flow; - - CFSocketContext ctx = { .info = (__bridge void *)bypass }; - bypass->m_socket = CFSocketCreate(kCFAllocatorDefault, family, SOCK_DGRAM, IPPROTO_UDP, - kCFSocketDataCallBack, udpSockCallback, &ctx); - - // TODO: If flow.remoteHostname is set should we turn this into a connected socket? - - // Bind the socket to the bypass interface. - int sockfd = CFSocketGetNative(bypass->m_socket); - int ifindex = nw_interface_get_index(interface); - if (family == AF_INET6) { - setsockopt(sockfd, IPPROTO_IPV6, IPV6_BOUND_IF, &ifindex, sizeof(ifindex)); - } else { - setsockopt(sockfd, IPPROTO_IP, IP_BOUND_IF, &ifindex, sizeof(ifindex)); - } - - // Bind the socket if a local port was specified. - // Note that this intentionally ignores the local address since we are - // binding to a specific interface anyways. - if (endpoint && (nw_endpoint_get_port(endpoint) != 0)) { - struct sockaddr_storage ss; - int port = nw_endpoint_get_port(endpoint); - memset(&ss, 0, sizeof(struct sockaddr_storage)); - if (family == AF_INET6) { - struct sockaddr_in6* sin6 = (struct sockaddr_in6 *)&ss; - sin6->sin6_family = AF_INET6; - sin6->sin6_len = sizeof(struct sockaddr_in6); - sin6->sin6_port = htons(port); - } else { - struct sockaddr_in* sin = (struct sockaddr_in *)&ss; - sin->sin_family = AF_INET; - sin->sin_len = sizeof(struct sockaddr_in); - sin->sin_port = htons(port); - } - -#if MZ_DEBUG - char* addrstr = nw_endpoint_copy_address_string(endpoint); - NSLog(@"udp bind to %s port %d", addrstr, port); - free(addrstr); -#endif - - CFDataRef addr = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const UInt8*)&ss, - ss.ss_len, kCFAllocatorNull); - CFSocketSetAddress(bypass->m_socket, addr); - CFRelease(addr); - } - - // Create a source and attach it to the main run loop. - bypass->m_source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, bypass->m_socket, 0); - CFRunLoopAddSource(CFRunLoopGetMain(), bypass->m_source, kCFRunLoopDefaultMode); - - return bypass; -} - -- (void)dealloc { - if (m_socket) { - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - m_socket = nil; - } - if (m_source) { - CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); - CFRelease(m_source); - m_source = nil; - } - -#if !__has_feature(objc_arc) - [super dealloc]; -#endif -} - -- (void)startBypass:(void (^)(NSError *error)) completionHandler { - if (@available(macOS 15, *)) { - NSLog(@"bypass opening"); - [self.flow openWithLocalFlowEndpoint:self.flow.localFlowEndpoint - completionHandler:^(NSError* openError){ - if (openError) { - NSLog(@"bypass open error: %@", openError); - //nw_connection_cancel(m_connection); - completionHandler(openError); - } else { - [self handleOutbound:completionHandler]; - } - }]; - } else if ([self.flow.localEndpoint isKindOfClass:[NWHostEndpoint class]]) { - NSLog(@"bypass opening (legacy bound)"); - [self.flow openWithLocalEndpoint:(NWHostEndpoint*)self.flow.localEndpoint - completionHandler:^(NSError* openError){ - if (openError) { - NSLog(@"bypass open error: %@", openError); - [self closeConnection:openError completionHandler:completionHandler]; - } else { - [self handleOutbound:completionHandler]; - } - }]; - } else if (self.flow.localEndpoint == nil) { - NSLog(@"bypass opening (legacy unbound)"); - [self.flow openWithLocalEndpoint:nil - completionHandler:^(NSError* openError){ - if (openError) { - NSLog(@"bypass open error: %@", openError); - [self closeConnection:openError completionHandler:completionHandler]; - } else { - [self handleOutbound:completionHandler]; - } - }]; - } else { - // Otherwise, we don't support split-tunneling this endpoint type. - // It's probably a bonjour endpoint. - // TODO: We should probably return NO from handleNewFlow in this case. - [self closeConnection:nil completionHandler:completionHandler]; - } -} - -- (void)handleOutbound:(void (^)(NSError *error)) completionHandler { - if (@available(macOS 15, *)) { - [self.flow readDatagramsAndFlowEndpointsWithCompletionHandler:^(NSArray *datagrams, NWEndpointArray *remoteEndpoints, NSError *err) { - if (err) { - NSLog(@"dgram error: %@", err); - [self closeConnection:err completionHandler:completionHandler]; - return; - } - - // If there was no data, try again. - if (!datagrams) { - [self handleOutbound:completionHandler]; - return; - } - - // Outbound data flow terminated gracefully. - if (datagrams.count == 0) { - [self closeConnection:nil completionHandler:completionHandler]; - return; - } - - // Process the outbound datagrams. - for (NSUInteger i = 0; i < datagrams.count; i++) { - [self sendDatagram:datagrams[i] toEndpoint:remoteEndpoints[i]]; - } - - // Datagrams handled, try again to continue processing the flow. - [self handleOutbound:completionHandler]; - }]; - } else { - [self.flow readDatagramsWithCompletionHandler:^(NSArray *datagrams, NSArray *remoteEndpoints, NSError *err) { - if (err) { - [self closeConnection:err completionHandler:completionHandler]; - return; - } - - // If there was no data, try again. - if (!datagrams) { - [self handleOutbound:completionHandler]; - return; - } - - // Outbound data flow terminated gracefully. - if (datagrams.count == 0) { - [self closeConnection:nil completionHandler:completionHandler]; - return; - } - - // Process the outbound datagrams. - for (NSUInteger i = 0; i < datagrams.count; i++) { - NWEndpoint* legacyEndpoint = remoteEndpoints[i]; - if ([legacyEndpoint isKindOfClass:[NWHostEndpoint class]]) { - NWHostEndpoint* host = (NWHostEndpoint*)legacyEndpoint; - nw_endpoint_t ep = nw_endpoint_create_host([host.hostname UTF8String], [host.port UTF8String]); - [self sendDatagram:datagrams[i] toEndpoint:ep]; - } - } - - // Datagrams handled, try again to continue processing the flow. - [self handleOutbound:completionHandler]; - }]; - } -} - -- (void)sendDatagram:(NSData*)dgram - toEndpoint:(nw_endpoint_t)ep { - // TODO: Handle address and URL endpoints? - if (nw_endpoint_get_type(ep) != nw_endpoint_type_address) { - // We cannot handle this destination address type. - NSLog(@"dgram: unsupported type %d", (int)nw_endpoint_get_type(ep)); - } else { - // Otherwise, it must be an address endpoint. - const struct sockaddr* sa = nw_endpoint_get_address(ep); - CFDataRef dst = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, (const UInt8*)sa, - sa->sa_len, kCFAllocatorNull); - CFSocketError err = CFSocketSendData(m_socket, dst, (CFDataRef)dgram, 0); - // TODO: Do we care about the error? - CFRelease(dst); - } -} - -- (void)recvDatagram:(NSData*)dgram - fromEndpoint:(nw_endpoint_t)ep { - if (@available(macOS 15, *)) { - [self.flow writeDatagrams:@[dgram] - sentByFlowEndpoints:@[ep] - completionHandler:^(NSError* error){ - if (error) { - // TODO: Handle the error? - } - }]; - } else { - char* addr = nw_endpoint_copy_address_string(ep); - NSString* hostname = [[NSString alloc] initWithBytesNoCopy:addr - length:strlen(addr) - encoding:NSUTF8StringEncoding - freeWhenDone:TRUE]; - NSString* port = [NSString stringWithFormat:@"%d", nw_endpoint_get_port(ep)]; - NWHostEndpoint* host = [NWHostEndpoint endpointWithHostname:hostname port:port]; - - [self.flow writeDatagrams:@[dgram] - sentByEndpoints:@[host] - completionHandler:^(NSError* error){ - if (error) { - // TODO: Handle the error? - } - }]; - } -} - -- (void)closeConnection:(NSError *)error - completionHandler:(void (^)(NSError *)) completionHandler { - NSLog(@"bypass close"); - // Close the flow. - if (self.flow) { - [self.flow closeReadWithError:error]; - [self.flow closeWriteWithError:error]; - self.flow = nil; - } - - // And the associated bypass socket. - if (m_socket) { - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - m_socket = nil; - } - if (m_source) { - CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); - CFRelease(m_source); - m_source = nil; - } - - if (completionHandler) { - completionHandler(error); - } -} - -@end From 038fe7dd667977c40ed635f9df87d0a91408bf3f Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Thu, 14 May 2026 14:09:09 -0700 Subject: [PATCH 30/31] Initial attempt at server switching support --- .../VPNSplitTunnelProvider.mm | 134 +++++++++++++----- macos/networkextension/utils.h | 8 +- macos/networkextension/utils.mm | 77 +++++++++- macos/networkextension/wireguardpeer.mm | 2 +- .../macos/macosextensioncontroller.mm | 43 ++++-- 5 files changed, 204 insertions(+), 60 deletions(-) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index 80c4a8c9bc..e06e742fd5 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -5,6 +5,7 @@ #import #import "interfaceconfig.h" +#import "utils.h" #import "wireguardtunnel.h" #include @@ -55,13 +56,6 @@ - (id)init{ return self; } -+ (NSError*) makeError:(NSInteger)code - withDescription:(NSString*)desc { - return [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] - code:code - userInfo:@{NSLocalizedDescriptionKey: desc}]; -} - + (nw_endpoint_t)convertEndpoint:(NWEndpoint*)old { if (old == nil) { return nil; @@ -149,13 +143,11 @@ - (void)startProxyWithOptions:(NSDictionary *)options m_handledUnknown = 0; // Parse the configuration - InterfaceConfig* config = [[InterfaceConfig alloc] initFromDict:options]; - if (!config) { - completionHandler([VPNSplitTunnelProvider makeError:1 - withDescription:@"invalid configuration"]); + _config = [[InterfaceConfig alloc] initFromDict:options]; + if (!self.config) { + completionHandler(vpnProviderError(NEProviderStopReasonConfigurationFailed)); return; } - _config = config; self.wireguard = [WireguardTunnel new]; self.settings = [[NETransparentProxyNetworkSettings alloc] initWithTunnelRemoteAddress:self.protocolConfiguration.serverAddress]; @@ -245,6 +237,12 @@ - (void)startProxyWithOptions:(NSDictionary *)options return; } + // Register a KVO observer to switch servers upon configuration change. + [weakSelf addObserver:weakSelf + forKeyPath:@"protocolConfiguration" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:nil]; + // Success completionHandler(nil); }]; @@ -260,10 +258,91 @@ - (void)stopProxyWithReason:(NEProviderStopReason)reason NSLog(@"handled udp flows: %lld", std::atomic_load(&m_handledUdpFlows)); NSLog(@"handled unknown flows: %lld", std::atomic_load(&m_handledUnknown)); + [self removeObserver:self + forKeyPath:@"protocolConfiguration"]; + [self.wireguard stopTunnelWithReason:reason completionHandler:completionHandler]; } +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + // The only thing we should be observing is the protocolConfiguration + if (![keyPath isEqual:@"protocolConfiguration"]) { + return; + } + NSLog(@"configuration changed"); + + // Parse and update the configuration + NETunnelProviderProtocol* proto = (NETunnelProviderProtocol*)self.protocolConfiguration; + _config = [[InterfaceConfig alloc] initFromDict:proto.providerConfiguration]; + if (!self.config) { + // We can't make sense of this configuration. + [self.wireguard stopTunnelWithReason:NEProviderStopReasonConfigurationFailed + completionHandler:^(){ + [self cancelProxyWithError:vpnProviderError(NEProviderStopReasonConfigurationFailed)]; + }]; + return; + } + + // Update the app exclusion settings. + [self.vpnDisabledApps removeAllObjects]; + NSArray* apps = [proto.providerConfiguration objectForKey:@"apps"]; + if (apps) { + NSEnumerator* iter = [apps objectEnumerator]; + while (id appId = [iter nextObject]) { + if (![appId isKindOfClass:[NSString class]]) { + continue; + } +#ifdef MZ_DEBUG + NSLog(@"excluding app %@ from VPN", appId); +#endif + [self.vpnDisabledApps addObject: appId]; + } + } + // TODO: We probably need to update/reset connections if they changed their exclusion status. + // but how? + + // YOLO: Does this do anything.... + [self performSelector:@selector(fetchFlowStatesWithCompletionHandler:) + withObject:^(NSArray* result){ + NSLog(@"flow count: %lu", result.count); + for (NSObject* obj in result) { + NSLog(@"flow state: %@", NSStringFromClass(obj.class)); + } + }]; + + // Check if the server identitiy changed. Do nothing if no changes. + NETunnelProviderProtocol* old = [change objectForKey:@"old"]; + if (old && [old isKindOfClass:NETunnelProviderProtocol.class]) { + NSString* oldPubKey = [old.providerConfiguration objectForKey:@"serverPublicKey"]; + if (oldPubKey && [oldPubKey isKindOfClass:NSString.class]) { + if ([oldPubKey isEqual:self.config.serverPublicKey]) { + return; + } + } + } + + // Shutdown the old wireguard peer and start a new one. + self.reasserting = TRUE; + [self.wireguard.peer stopWithReason:NEProviderStopReasonSuperceded + completionHandler:^(){ + // Create and start a new peer. + self.wireguard.peer = [[WireguardPeer alloc] initWithOptions:self.config + andTunnel:self.wireguard]; + [self.wireguard.peer startWithOptions:self.config + completionHandler:^(NSError*err){ + if (err) { + [self cancelProxyWithError:err]; + } else { + self.reasserting = FALSE; + } + }]; + }]; +} + - (BOOL)matchAppFlow:(NEAppProxyFlow*)flow { // Without metadata - always direct the flow into the VPN. if (flow.metaData == nil) { @@ -337,14 +416,14 @@ - (void)handleAppMessage:(NSData *)messageData error:&error]; if (error != nil) { NSLog(@"app message error: %@", error.localizedDescription); - [VPNSplitTunnelProvider sendAppError:error completionHandler:completionHandler]; + [VPNSplitTunnelProvider sendAppResponse:error completionHandler:completionHandler]; return; } NSString* action = [msg decodeObjectOfClass:NSString.class forKey:@"action"]; if (!action) { NSLog(@"app message invalid action"); - NSError* error = [VPNSplitTunnelProvider makeError:1 withDescription:@"invalid app message invalid"]; - [VPNSplitTunnelProvider sendAppError:error completionHandler:completionHandler]; + NSError* error = vpnProviderError(NEProviderStopReasonConfigurationFailed); + [VPNSplitTunnelProvider sendAppResponse:error completionHandler:completionHandler]; return; } @@ -366,8 +445,8 @@ - (void)handleAppMessage:(NSData *)messageData // Wireguard Tunnel messages if ([action isEqualToString:@"status"]) { - [VPNSplitTunnelProvider sendAppObject:self.wireguard.status - completionHandler:completionHandler]; + [VPNSplitTunnelProvider sendAppResponse:self.wireguard.status + completionHandler:completionHandler]; return; } @@ -385,33 +464,20 @@ - (void)handleAppMessage:(NSData *)messageData [VPNSplitTunnelProvider sendAppResponse:nil completionHandler:completionHandler]; } -+ (void)sendAppResponse:(NSData*) responseData ++ (void)sendAppResponse:(id) obj completionHandler:(void (^)(NSData*)) completionHandler { if (!completionHandler) { return; } - completionHandler(responseData); -} -+ (void)sendAppObject:(id) obj - completionHandler:(void (^)(NSData*)) completionHandler { NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; - if ([obj respondsToSelector:@selector(encodeWithCoder:)]) { + if ([obj isKindOfClass:[NSError class]]) { + [encoder encodeObject:obj forKey:@"error"]; + } else if ([obj respondsToSelector:@selector(encodeWithCoder:)]) { [obj encodeWithCoder: encoder]; } [encoder finishEncoding]; completionHandler(encoder.encodedData); } -+ (void)sendAppError:(NSError*) error - completionHandler:(void (^)(NSData*)) completionHandler { - if (!completionHandler) { - return; - } - NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; - [encoder encodeObject:error forKey:@"error"]; - [encoder finishEncoding]; - completionHandler(encoder.encodedData); -} - @end diff --git a/macos/networkextension/utils.h b/macos/networkextension/utils.h index 56627b31aa..79068ec417 100644 --- a/macos/networkextension/utils.h +++ b/macos/networkextension/utils.h @@ -8,15 +8,9 @@ #ifndef UTILS_H -typedef enum { - kVPNSuccess = 0, - kVPNErrInvalidConfig = 1, - kVPNErrTunnelNotRunning = 2, -} VPNErrorType; - nw_endpoint_t convertEndpoint(NWEndpoint* ep); NSUInteger getWorkerCount(); -NSError* vpnProviderError(VPNErrorType err, NSString* msg); +NSError* vpnProviderError(NEProviderStopReason reason); NSError* vpnPosixError(int code, NSString* msg); #endif // UTILS_H diff --git a/macos/networkextension/utils.mm b/macos/networkextension/utils.mm index f85a7e5ab5..b97bb476d7 100644 --- a/macos/networkextension/utils.mm +++ b/macos/networkextension/utils.mm @@ -62,10 +62,81 @@ nw_endpoint_t convertEndpoint(NWEndpoint* old) { return nw_endpoint_create_host(host.hostname.UTF8String, host.port.UTF8String); } -NSError* vpnProviderError(VPNErrorType err, NSString* description) { +NSError* vpnProviderError(NEProviderStopReason reason) { + NSDictionary* info = nil; + switch (reason) { + case NEProviderStopReasonNone: + info = @{NSLocalizedDescriptionKey: @"No specific reason"}; + break; + + case NEProviderStopReasonUserInitiated: + info = @{NSLocalizedDescriptionKey: @"The user stopped the provider extension"}; + break; + + case NEProviderStopReasonProviderFailed: + info = @{NSLocalizedDescriptionKey: @"The provider failed to function correctly"}; + break; + + case NEProviderStopReasonNoNetworkAvailable: + info = @{NSLocalizedDescriptionKey: @"No network connectivity is currently available"}; + break; + + case NEProviderStopReasonUnrecoverableNetworkChange: + info = @{NSLocalizedDescriptionKey: @"The device's network connectivity changed"}; + break; + + case NEProviderStopReasonProviderDisabled: + info = @{NSLocalizedDescriptionKey: @"The provider was disabled"}; + break; + + case NEProviderStopReasonAuthenticationCanceled: + info = @{NSLocalizedDescriptionKey: @"The authentication process was canceled"}; + break; + + case NEProviderStopReasonConfigurationFailed: + info = @{NSLocalizedDescriptionKey: @"The configuration is invalid"}; + break; + + case NEProviderStopReasonIdleTimeout: + info = @{NSLocalizedDescriptionKey: @"The session timed out"}; + break; + + case NEProviderStopReasonConfigurationDisabled: + info = @{NSLocalizedDescriptionKey: @"The configuration was disabled"}; + break; + + case NEProviderStopReasonConfigurationRemoved: + info = @{NSLocalizedDescriptionKey: @"The configuration was removed"}; + break; + + case NEProviderStopReasonSuperceded: + info = @{NSLocalizedDescriptionKey: @"The configuration was superceded by a higher-priority configuration"}; + break; + + case NEProviderStopReasonUserLogout: + info = @{NSLocalizedDescriptionKey: @"The user logged out"}; + break; + + case NEProviderStopReasonConnectionFailed: + info = @{NSLocalizedDescriptionKey: @"The connection failed"}; + break; + + case NEProviderStopReasonSleep: + info = @{NSLocalizedDescriptionKey: @"A stop reason indicating the configuration enabled disconnect on sleep and the device went to sleep"}; + break; + + case NEProviderStopReasonInternalError: + info = @{NSLocalizedDescriptionKey: @"The provider encountered an internal error"}; + break; + + case NEProviderStopReasonAppUpdate: + default: + break; + } + return [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] - code:(NSUInteger)err - userInfo:@{NSLocalizedDescriptionKey: description}]; + code:(NSUInteger)reason + userInfo:info]; } NSError* vpnPosixError(int code, NSString* desc) { diff --git a/macos/networkextension/wireguardpeer.mm b/macos/networkextension/wireguardpeer.mm index f349f28c5a..b895bc56e2 100644 --- a/macos/networkextension/wireguardpeer.mm +++ b/macos/networkextension/wireguardpeer.mm @@ -170,7 +170,7 @@ - (void) socketWorker:(id)arg { - (void) stopWithReason:(NEProviderStopReason)reason completionHandler:(void (^)()) completionHandler { - [self cancelWithError:vpnPosixError(ECANCELED, @"wireguard peer stopped")]; + [self cancelWithError:vpnProviderError(reason)]; completionHandler(); } diff --git a/src/platforms/macos/macosextensioncontroller.mm b/src/platforms/macos/macosextensioncontroller.mm index b95187911c..933f11fcee 100644 --- a/src/platforms/macos/macosextensioncontroller.mm +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -190,19 +190,32 @@ - (void)notifyStatusChanged:(NSNotification*)notify; } [options setObject:vpnDisabledApps forKey:@"apps"]; - // Start the split tunnel proxy. - NSError* error = nil; - NETunnelProviderSession* session = - static_cast(m_manager.connection); - BOOL okay = [session startTunnelWithOptions:options andReturnError:&error]; - if (error) { - logger.warning() << "proxy start error:" << error.localizedDescription; - } else if (!okay) { - logger.warning() << "proxy start failed"; - } else { - // Save the session and retain it. - m_session = [session retain]; - } + // Update the tunnel configuration. + NETunnelProviderProtocol* proto = + static_cast(m_manager.protocolConfiguration); + proto.providerConfiguration = options; + [m_manager saveToPreferencesWithCompletionHandler:^(NSError* error) { + if (error) { + logger.warning() << "prefs update error:" << error.localizedDescription; + return; + } + + // If we don't already have a session - start one. + if (m_session) { + return; + } + NETunnelProviderSession* session = + static_cast(m_manager.connection); + BOOL okay = [session startTunnelWithOptions:options andReturnError:&error]; + if (error) { + logger.warning() << "proxy start error:" << error.localizedDescription; + } else if (!okay) { + logger.warning() << "proxy start failed"; + } else { + // Save the session and retain it. + m_session = [session retain]; + } + }]; } void MacOSExtensionController::deactivate() { @@ -304,8 +317,8 @@ - (void)notifyEnabledChanged:(NSNotification*)notify { } - (void)notifyStatusChanged:(NSNotification*)notify { - NEVPNStatus status = static_cast(notify.object).status; - QMetaObject::invokeMethod(self.parent, "extStatusChange", Q_ARG(int, status)); + NEVPNConnection* conn = static_cast(notify.object); + QMetaObject::invokeMethod(self.parent, "extStatusChange", Q_ARG(int, conn.status)); } @end From ecb8c7f6e7cdb87d5ae8d3bf4b88b50e09b08865 Mon Sep 17 00:00:00 2001 From: Naomi Kirby Date: Tue, 28 Apr 2026 15:31:50 -0700 Subject: [PATCH 31/31] Fixup status generation for NetmgrController --- src/platforms/linux/netmgrcontroller.cpp | 18 +++++++++++------- src/platforms/linux/netmgrcontroller.h | 7 +++++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/platforms/linux/netmgrcontroller.cpp b/src/platforms/linux/netmgrcontroller.cpp index 5a6ccc1fbd..ae16f7793d 100644 --- a/src/platforms/linux/netmgrcontroller.cpp +++ b/src/platforms/linux/netmgrcontroller.cpp @@ -66,7 +66,8 @@ void NetmgrController::initialize(const Device* device, const Keys* keys) { } m_uuid = uuid.toString(QUuid::WithoutBraces); - m_deviceIpv4Address = device->ipv4Address(); + m_deviceIpv4Address = QHostAddress(device->ipv4Address().split('/').first()); + m_deviceIpv6Address = QHostAddress(device->ipv6Address().split('/').first()); // Generic connection settings. m_config.insert("id", QCoreApplication::applicationName()); @@ -248,11 +249,16 @@ void NetmgrController::activate(const InterfaceConfig& config, m_ipv6config.insert("route-data", ipv6routes); m_wireguard.insert("peers", peers); + // Keep the server details for later. + m_serverPublicKey = config.m_serverPublicKey; + m_serverIpv4Gateway = QHostAddress(config.m_serverIpv4Gateway); + m_serverIpv6Gateway = QHostAddress(config.m_serverIpv6Gateway); + // Update the DNS server. if ((config.m_dnsServer == config.m_serverIpv4Gateway) || (config.m_dnsServer == config.m_serverIpv6Gateway)) { - setDnsConfig(m_ipv4config, QHostAddress(config.m_serverIpv4Gateway)); - setDnsConfig(m_ipv6config, QHostAddress(config.m_serverIpv6Gateway)); + setDnsConfig(m_ipv4config, m_serverIpv4Gateway); + setDnsConfig(m_ipv6config, m_serverIpv6Gateway); } else if (config.m_dnsServer.contains(':')) { setDnsConfig(m_ipv4config, QHostAddress()); setDnsConfig(m_ipv6config, QHostAddress(config.m_dnsServer)); @@ -261,10 +267,6 @@ void NetmgrController::activate(const InterfaceConfig& config, setDnsConfig(m_ipv6config, QHostAddress()); } - // Keep the server details for later. - m_serverPublicKey = config.m_serverPublicKey; - m_serverIpv4Gateway = config.m_serverIpv4Gateway; - // Update the connection settings. QList args; args << serializeConfig(); @@ -395,7 +397,9 @@ void NetmgrController::checkStatus() { st.m_connected = true; st.m_timestamp = guessUptime(); st.m_ipv4Gateway = m_serverIpv4Gateway; + st.m_ipv6Gateway = m_serverIpv6Gateway; st.m_ipv4Address = m_deviceIpv4Address; + st.m_ipv6Address = m_deviceIpv6Address; st.m_rxBytes = readSysfsFile(rxPath); st.m_txBytes = readSysfsFile(txPath); diff --git a/src/platforms/linux/netmgrcontroller.h b/src/platforms/linux/netmgrcontroller.h index 885e55fb61..5a7710f776 100644 --- a/src/platforms/linux/netmgrcontroller.h +++ b/src/platforms/linux/netmgrcontroller.h @@ -6,6 +6,7 @@ #define NETWORKMANAGERCONTROLLER_H #include +#include #include #include #include @@ -75,8 +76,10 @@ class NetmgrController final : public ControllerImpl { QVariantMap m_wireguard; QString m_serverPublicKey; - QString m_serverIpv4Gateway; - QString m_deviceIpv4Address; + QHostAddress m_serverIpv4Gateway; + QHostAddress m_serverIpv6Gateway; + QHostAddress m_deviceIpv4Address; + QHostAddress m_deviceIpv6Address; QString m_uuid; QVersionNumber m_version;