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 diff --git a/macos/networkextension/CMakeLists.txt b/macos/networkextension/CMakeLists.txt index df36936133..dc313a0581 100644 --- a/macos/networkextension/CMakeLists.txt +++ b/macos/networkextension/CMakeLists.txt @@ -52,19 +52,42 @@ 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 - ${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 - ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.h - ${CMAKE_CURRENT_SOURCE_DIR}/routemanager.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 + ${CMAKE_CURRENT_SOURCE_DIR}/wireguardtunnel.mm ${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) diff --git a/macos/networkextension/VPNSplitTunnelProvider.mm b/macos/networkextension/VPNSplitTunnelProvider.mm index d8e452b994..e06e742fd5 100644 --- a/macos/networkextension/VPNSplitTunnelProvider.mm +++ b/macos/networkextension/VPNSplitTunnelProvider.mm @@ -4,9 +4,9 @@ #import -#import "bypasstcpflow.h" -#import "bypassudpflow.h" -#import "routemanager.h" +#import "interfaceconfig.h" +#import "utils.h" +#import "wireguardtunnel.h" #include #include @@ -17,7 +17,7 @@ #include #include -@interface VPNSplitTunnelProvider : NETransparentProxyProvider +@interface VPNSplitTunnelProvider : NETransparentProxyProvider - (void)startProxyWithOptions:(NSDictionary *)options completionHandler:(void (^)(NSError *))completionHandler; @@ -29,15 +29,9 @@ - (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) nw_interface_t ipv4Interface; -@property (strong) nw_interface_t ipv6Interface; -@property (strong) nw_interface_t vpnInterface; +@property (strong) WireguardTunnel* wireguard; +@property (strong) InterfaceConfig* config; @property (strong) NSMutableArray* vpnDisabledApps; @@ -62,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; @@ -155,10 +142,14 @@ - (void)startProxyWithOptions:(NSDictionary *)options m_handledUdpFlows = 0; m_handledUnknown = 0; - // Start the route manager - _routeManager = [RouteManager new]; - [self.routeManager startWithDelegate:self]; + // Parse the configuration + _config = [[InterfaceConfig alloc] initFromDict:options]; + if (!self.config) { + completionHandler(vpnProviderError(NEProviderStopReasonConfigurationFailed)); + return; + } + self.wireguard = [WireguardTunnel new]; self.settings = [[NETransparentProxyNetworkSettings alloc] initWithTunnelRemoteAddress:self.protocolConfiguration.serverAddress]; // Configure the proxy to capture all traffic @@ -231,79 +222,139 @@ - (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:^(NSError* wgError){ + if (wgError != nil) { + completionHandler(wgError); + return; + } + + // Register a KVO observer to switch servers upon configuration change. + [weakSelf addObserver:weakSelf + forKeyPath:@"protocolConfiguration" + options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew + context:nil]; + + // Success + completionHandler(nil); + }]; } - completionHandler(error); }]; } - (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); - 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.ipv4Interface = nil; + NSLog(@"stopping proxy"); + + 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)); + + [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]; } - 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)]; - - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dst - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv6Interface) - withGateway:nil - andFlags:RTF_IFSCOPE]; - - self.ipv6Interface = nil; + } + // 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)); } - 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)); + // 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; + } + } + } - completionHandler(); + // 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 - 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; @@ -327,90 +378,37 @@ - (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; } // Perform flow bypassing. + flow.networkInterface = self.wireguard.virtualInterface; 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]; - 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_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]; - 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 { +- (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 = @@ -418,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; } @@ -445,6 +443,14 @@ - (void)handleAppMessage:(NSData *) messageData } [msg finishDecoding]; + // Wireguard Tunnel messages + if ([action isEqualToString:@"status"]) { + [VPNSplitTunnelProvider sendAppResponse:self.wireguard.status + completionHandler:completionHandler]; + return; + } + + // Application exclusion messages. if ([action isEqualToString: @"clear"]) { [self.vpnDisabledApps removeAllObjects]; } else if ([action isEqualToString: @"add"]) { @@ -458,109 +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)sendAppError:(NSError*) error - completionHandler:(void (^)(NSData*)) completionHandler { - if (!completionHandler) { - return; - } NSKeyedArchiver* encoder = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; - [encoder encodeObject:error forKey:@"error"]; + 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)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) { - struct sockaddr_in dst; - 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) { - 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 { - action = RTM_ADD; - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dstAddr - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv4Interface) - withGateway:nil - andFlags:RTF_IFSCOPE]; - } - } else if (self.ipv4Interface) { - NSLog(@"default ipv4 route lost"); - action = RTM_DELETE; - ifindex = nw_interface_get_index(self.ipv4Interface); - } - - // 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) { - 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; - [self.routeManager rtmSendRoute:RTM_DELETE - toDestination:dstAddr - withPrefix:0 - viaInterface:nw_interface_get_index(self.ipv6Interface) - withGateway:nil - 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) { - [self.routeManager rtmSendRoute:action - toDestination:dstAddr - withPrefix:0 - viaInterface:ifindex - withGateway:gateway - andFlags:RTF_IFSCOPE]; - } - } -} - @end 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 diff --git a/macos/networkextension/interfaceconfig.h b/macos/networkextension/interfaceconfig.h new file mode 100644 index 0000000000..0d536b0bd9 --- /dev/null +++ b/macos/networkextension/interfaceconfig.h @@ -0,0 +1,36 @@ +/* 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 RoutePrefix : NSObject + ++ (id)parseRoute:(NSString *)routeString; + +@property (readonly, getter=getDestination) const struct sockaddr* destination; +@property (readonly) NSUInteger prefixLength; +@end + +@interface InterfaceConfig : NSObject + +- (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 (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; + +@property (strong, readonly) NSDictionary* dict; + +@end diff --git a/macos/networkextension/interfaceconfig.mm b/macos/networkextension/interfaceconfig.mm new file mode 100644 index 0000000000..688f447dea --- /dev/null +++ b/macos/networkextension/interfaceconfig.mm @@ -0,0 +1,178 @@ +/* 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 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 +- (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; +} + +- (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 { + InterfaceConfig* config = [InterfaceConfig new]; + config->_dict = dict; + + config.privateKey = [config findString:@"privateKey"]; + if (!config.privateKey) { + return nil; + } + config.deviceIpv4Addr = [config findAddress:@"deviceIpv4Addr" withPort:0]; + if ((!config.deviceIpv4Addr) || + (nw_endpoint_get_address(config.deviceIpv4Addr)->sa_family != AF_INET)) { + return nil; + } + config.deviceIpv6Addr = [config findAddress:@"deviceIpv6Addr" withPort:0]; + if ((!config.deviceIpv6Addr) || + (nw_endpoint_get_address(config.deviceIpv6Addr)->sa_family != AF_INET6)) { + return nil; + } + + 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; + } + + config.serverIpv4Addr = [config findAddress:@"serverIpv4AddrIn" withPort:config.serverPort]; + config.serverIpv6Addr = [config findAddress:@"serverIpv6AddrIn" withPort:config.serverPort]; + + // 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"]; + NSMutableArray* routes = [NSMutableArray new]; + if ([rangesObject isKindOfClass:[NSArray class]]) { + NSArray* list = (NSArray*)rangesObject; + for (NSString* rangeString in list) { + RoutePrefix* prefix = [RoutePrefix parseRoute:rangeString]; + if (!prefix) { + return nil; + } + [routes addObject:prefix]; + } + } + config.routes = [NSArray arrayWithArray:routes]; + + 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/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 8123bf72a3..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:(NSData*)dst - withPrefix:(unsigned int)plen - viaInterface:(unsigned int)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 579fd885c1..0000000000 --- a/macos/networkextension/routemanager.mm +++ /dev/null @@ -1,505 +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. - CFSocketNativeHandle m_sockfd; - CFSocketRef m_socket; - CFRunLoopSourceRef m_source; - 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; - m_sockfd = socket(PF_ROUTE, SOCK_RAW, 0); - if (m_sockfd < 0) { - NSLog(@"failed to create routing socket: %s", strerror(errno)); - } - CFSocketContext ctx = { .info = (__bridge void *)self }; - m_socket = CFSocketCreateWithNative(kCFAllocatorDefault, m_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); - - return self; -} - -- (void)dealloc { - NSLog(@"route manager destroyed"); - - CFSocketInvalidate(m_socket); - CFRelease(m_socket); - close(m_sockfd); - - CFRunLoopRemoveSource(CFRunLoopGetMain(), m_source, kCFRunLoopDefaultMode); - CFRelease(m_source); - -#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:(NSData*)dst - withPrefix:(unsigned int)plen - viaInterface:(unsigned int)ifindex - 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 = ifindex; - 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 - const struct sockaddr* dstAddr = (const struct sockaddr*)dst.bytes; - rtmAppendAddr(rtm, rtm_max_size, RTA_DST, dstAddr); - - // 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); - } - - // Append RTA_NETMASK - if (dstAddr->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 (dstAddr->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(m_sockfd, 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/utils.h b/macos/networkextension/utils.h new file mode 100644 index 0000000000..79068ec417 --- /dev/null +++ b/macos/networkextension/utils.h @@ -0,0 +1,16 @@ +/* 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 + +nw_endpoint_t convertEndpoint(NWEndpoint* ep); +NSUInteger getWorkerCount(); +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 new file mode 100644 index 0000000000..b97bb476d7 --- /dev/null +++ b/macos/networkextension/utils.mm @@ -0,0 +1,153 @@ +/* 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(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)reason + userInfo:info]; +} + +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..de28ababe0 --- /dev/null +++ b/macos/networkextension/wireguardpeer.h @@ -0,0 +1,42 @@ +/* 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; + +extern "C" struct wireguard_tunnel; + +@class WireguardTunnel; + +@interface WireguardPeer : NSObject + +- (id) initWithOptions:(InterfaceConfig*) options + andTunnel:(WireguardTunnel*) tunnel; + +- (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) writePacket:(int)protocol + withData:(NSData*)data; + +@property (strong, readonly, getter=getStatus) WireguardStatus* status; +@property (weak, readonly) WireguardTunnel* tunnel; +@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..b895bc56e2 --- /dev/null +++ b/macos/networkextension/wireguardpeer.mm @@ -0,0 +1,303 @@ +/* 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" +#include "wireguardtunnel.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 { + // The wireguard tunnel provided by boringtun. + struct wireguard_tunnel* m_wireguard; + + CFRunLoopTimerRef m_timer; + struct timespec m_lastHandshake; + struct timespec m_handshakeTimeout; + + int m_socket; + 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); +} + +static void wgTimerCallback(CFRunLoopTimerRef t, void *info) { + WireguardPeer* peer = (__bridge WireguardPeer*)info; + [peer handleTimer]; +} + +- (id) initWithOptions:(InterfaceConfig*) options + andTunnel:(WireguardTunnel*) tunnel { + self = [super init]; + self->m_tunfd = tunnel.tunfd; + self->_tunnel = tunnel; + + m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (m_socket < 0) { + return nil; + } + + 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)); + if (!m_wireguard) { + return nil; + } + + // Give the socket its own worker. + self->m_worker = [[NSThread alloc] initWithTarget:self + selector:@selector(socketWorker:) + object: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; + } + + // 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, + wgTimerCallback, &timerContext); + CFRunLoopAddTimer(CFRunLoopGetMain(), m_timer, kCFRunLoopDefaultMode); + + // Force an initial handshake. + [self renegotiate:completionHandler]; + + // Start the socket worker + [m_worker start]; +} + +- (void) socketWorker:(id)arg { + NSLog(@"socket worker started"); + + 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; + + uint8_t plaintext[PEER_DATAGRAM_BUFSIZE]; + struct wireguard_result result; + 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(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) { + 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 + completionHandler:(void (^)()) completionHandler { + [self cancelWithError:vpnProviderError(reason)]; + completionHandler(); +} + +- (void) cancelWithError:(NSError*)error { + if (m_worker) { + [m_worker cancel]; + } + 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); + 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) 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, plaintext, 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/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 new file mode 100644 index 0000000000..51f76485db --- /dev/null +++ b/macos/networkextension/wireguardtunnel.h @@ -0,0 +1,35 @@ +/* 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 "wireguardpeer.h" +#import "wireguardstatus.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; + +- (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; +@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..15fab994a9 --- /dev/null +++ b/macos/networkextension/wireguardtunnel.mm @@ -0,0 +1,511 @@ +/* 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 +#include +#include +#include +#include +#include + +#import "utils.h" + +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 { + int m_tunfd; + dispatch_semaphore_t m_semaphore; + NSThread * m_worker; + + // 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; +} + +constexpr const int64_t WG_WORKQUEUE_TIMEOUT = 5LL * 1000000000LL; + +static void wgLog(const char* msg) { + NSLog(@"wg: %s", msg); +} + +- (id)init { + self = [super init]; + set_logging_function(wgLog); + + m_tunfd = -1; + m_rtseq = 0; + m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0); + m_tunfd = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); + if (m_tunfd < 0) { + return nil; + } + + // Connect to the utun control kernel service. + struct ctl_info info = {.ctl_name = UTUN_CONTROL_NAME}; + int err = ioctl(m_tunfd, CTLIOCGINFO, &info); + if (err < 0) { + close(m_tunfd); + return nil; + } + 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) { + close(m_tunfd); + 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); + err = getsockopt(m_tunfd, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifr.ifr_name, + &ifnamesize); + if (err < 0) { + 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); + + // 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; + } + + // Start outbound encryption workers. + m_semaphore = dispatch_semaphore_create(0); + m_worker = [[NSThread alloc] initWithTarget:self + selector:@selector(tunnelWorker:) + object:nil]; + [m_worker start]; + + return self; +} + +- (void) startTunnelWithOptions:(InterfaceConfig *)options + completionHandler:(void (^)(NSError *error)) completionHandler { + m_ipv4gateway = options.serverIpv4Gateway; + m_ipv6gateway = options.serverIpv6Gateway; + + // Assign addresses + if (NSError *err = [self setTunnelAddress:options.deviceIpv4Addr]) { + completionHandler(err); + return; + } + if (NSError *err = [self setTunnelAddress:options.deviceIpv6Addr]) { + completionHandler(err); + return; + } + + // Configure the peer + self.peer = [[WireguardPeer alloc] initWithOptions:options andTunnel:self]; + [self.peer startWithOptions:options completionHandler:^(NSError* error){ + NSLog(@"handshake completed"); + if (error) { + completionHandler(error); + return; + } + + // Update the MTU once the socket is open + //self.mtu = nw_connection_get_maximum_datagram_size(self.peer.connection) - WG_PACKET_OVERHEAD; + + // 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; + } + 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 { + if (!self.peer) { + [self shutdownTunnel]; + completionHandler(); + } + + [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 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 vpnPosixError(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 = vpnPosixError(errno, @"failed to set tunnel address"); + } else { + self.ipv4address = endpoint; + } + } 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 = vpnPosixError(errno, @"failed to set tunnel address"); + } else { + self.ipv6address = endpoint; + } + } else { + // We don't recognize this address type. + err = vpnPosixError(EAFNOSUPPORT, @"failed to set tunnel address"); + } + close(sock); + return err; +} + +- (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 = buffer.mutableBytes; + iov[1].iov_len = mtu; + + 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 worker. + if (m_worker) { + shutdown(m_tunfd, SHUT_RDWR); + [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) { + close(m_tunfd); + m_tunfd = -1; + } + + if (m_rtsock >= 0) { + close(m_rtsock); + m_rtsock = -1; + } +} + +- (void)cancelTunnelWithError:(NSError*)error { + if (self.peer) { + [self.peer cancelWithError:error]; + self.peer = nil; + } + + [self shutdownTunnel]; +} + +- (int)getTunfd { + return m_tunfd; +} + +- (WireguardStatus*)getStatus { + if (!self.peer) { + return [WireguardStatus new]; + } + WireguardStatus* result = self.peer.status; + + // 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]; + } + + return result; +} + +- (void)setMtu:(NSUInteger)mtu { + _mtu = mtu; + + struct ifreq ifr; + 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) { + NSLog(@"mtu update failed: %s", strerror(errno)); + } +} + +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); + 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; + } + } 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); + } else { + rtm->rtm_flags |= RTF_HOST; + } + } + + // 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)); +} + +- (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 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) 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/connectionhealth.cpp b/src/connectionhealth.cpp index 506be008cb..047b27ca45 100644 --- a/src/connectionhealth.cpp +++ b/src/connectionhealth.cpp @@ -92,8 +92,7 @@ void ConnectionHealth::stop() { m_dnsPingTimer.stop(); } -void ConnectionHealth::startActive(const QString& serverIpv4Gateway, - const QString& deviceIpv4Address) { +void ConnectionHealth::startActive(const ControllerStatus& status) { logger.debug() << "ConnectionHealth active started"; bool isNotOnOrOnPartial = @@ -101,14 +100,14 @@ void ConnectionHealth::startActive(const QString& serverIpv4Gateway, MozillaVPN::instance()->controller()->state() != Controller::StateOnPartial; - if (serverIpv4Gateway.isEmpty() || 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, deviceIpv4Address); + 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,16 +174,10 @@ void ConnectionHealth::connectionStateChanged() { switch (state) { 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); - - stop(); - startActive(serverIpv4Gateway, deviceIpv4Address); - }); + QObject::connect(controller, &Controller::statusUpdated, this, + &ConnectionHealth::startActive, + Qt::SingleShotConnection); + controller->refreshStatus(); break; case Controller::StateOff: @@ -265,11 +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"; - startActive(m_currentGateway, m_deviceAddress); + 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: diff --git a/src/connectionhealth.h b/src/connectionhealth.h index 2637c8c736..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 QString& serverIpv4Gateway, - const QString& deviceIpv4Address); + void startActive(const ControllerStatus& status); void startIdle(); void pingSentAndReceived(qint64 msec); @@ -89,8 +90,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 724394d462..bcd12dfcac 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -5,6 +5,8 @@ #include "controller.h" #include +#include +#include #include #include "constants.h" @@ -40,6 +42,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 +141,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) @@ -160,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, @@ -833,40 +844,14 @@ void Controller::cleanupBackendLogs() { } } -void Controller::getStatus( - std::function&& a_callback) { +void Controller::refreshStatus() { logger.debug() << "check status"; - std::function - callback = std::move(a_callback); - - 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 QString& serverIpv4Gateway, - const QString& deviceIpv4Address, - uint64_t txBytes, uint64_t rxBytes) { - logger.debug() << "Status updated"; - QList > - list; - - list.swap(m_getStatusCallbacks); - for (const std::function&func : list) { - func(serverIpv4Gateway, deviceIpv4Address, txBytes, rxBytes); + } else { + // Nothing changed - but emit it anyways. + emit statusUpdated(m_status); } } @@ -1166,3 +1151,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..c5ac4efb4c 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) @@ -134,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; @@ -155,10 +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; @@ -192,14 +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 QString& serverIpv4Gateway, - const QString& deviceIpv4Address, uint64_t txBytes, - uint64_t rxBytes); void implInitialized(bool status, bool connected, const QDateTime& connectionDate); void implPermRequired(); @@ -208,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(); @@ -284,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; @@ -317,11 +342,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..e42d8c9117 100644 --- a/src/platforms/ios/ioscontroller.mm +++ b/src/platforms/ios/ioscontroller.mm @@ -267,8 +267,14 @@ 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; + + emit statusUpdated(st); }]; } 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..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(); @@ -390,10 +392,18 @@ 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_ipv6Gateway = m_serverIpv6Gateway; + st.m_ipv4Address = m_deviceIpv4Address; + st.m_ipv6Address = m_deviceIpv6Address; + st.m_rxBytes = readSysfsFile(rxPath); + st.m_txBytes = readSysfsFile(txPath); + + emit statusUpdated(st); } QDateTime NetmgrController::guessUptime() { 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; diff --git a/src/platforms/macos/macoscontroller.h b/src/platforms/macos/macoscontroller.h index 0fa5d0a930..4ad747fc0b 100644 --- a/src/platforms/macos/macoscontroller.h +++ b/src/platforms/macos/macoscontroller.h @@ -34,8 +34,6 @@ class MacOSController final : public ControllerImpl { bool multihopSupported() override { return true; } - bool splitTunnelSupported() const override; - private slots: void upgradeService(); void registerService(); @@ -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 c1249e6a85..94e546d3e1 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,67 +221,17 @@ 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_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() { [remoteObject() getStatus:^(NSString* status){ QByteArray jsBlob = QString::fromNSString(status).toUtf8(); QJsonObject obj = QJsonDocument::fromJson(jsBlob).object(); - emitStatusFromJson(obj); + emit statusUpdated(ControllerStatus(obj)); }]; } @@ -342,48 +266,6 @@ emit initialized(true, jsObj.value("connected").toBool(), [remoteObject() cleanupBackendLogs]; } -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..d78418a7a1 --- /dev/null +++ b/src/platforms/macos/macosextensioncontroller.h @@ -0,0 +1,56 @@ +/* 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(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 + 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(); + void extEnabledChange(bool value); + void extStatusChange(int status); + + private: + static NSString* extIdentifier(); + + static QString parseArchivedString(NSCoder* archive, NSString* key); + static QHostAddress parseArchivedAddress(NSCoder* archive, NSString* key); + + private: + QString m_serverPublicKey; + + MacOSExtensionDelegate* 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..933f11fcee --- /dev/null +++ b/src/platforms/macos/macosextensioncontroller.mm @@ -0,0 +1,324 @@ +/* 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; +- (void)notifyEnabledChanged:(NSNotification*)notify; +- (void)notifyStatusChanged:(NSNotification*)notify; +@end + +namespace { +Logger logger("MacOSExtensionController"); +} // namespace + +MacOSExtensionController::MacOSExtensionController() : ControllerImpl() { + // Create the system extension loader delegate. + m_delegate = [[MacOSExtensionDelegate alloc] initWithObject:this]; + [m_delegate retain]; +} + +MacOSExtensionController::~MacOSExtensionController() { + [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 = 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; + + // 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){ + 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; + } + 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()); + } + }]; + }]; + }]; +} + +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::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); + + // Create a new tunnel provider session. + if ((m_manager == nil) || !m_manager.enabled) { + // Split tunnelling is not supported. + 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"]; + [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"]; + + 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()]; + for (const QString& appId : config.m_vpnDisabledApps) { + [vpnDisabledApps addObject:appId.toNSString()]; + } + [options setObject:vpnDisabledApps forKey:@"apps"]; + + // 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() { + if (m_session) { + // Stop the split tunnel proxy. + [m_session stopTunnel]; + [m_session release]; + m_session = nullptr; + } +} + +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. + 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){ + 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() << "status request failed:" << error; + emit statusUpdated(ControllerStatus()); + 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)); +} + +- (void)notifyEnabledChanged:(NSNotification*)notify { + bool enabled = static_cast(notify.object).enabled; + QMetaObject::invokeMethod(self.parent, "extEnabledChange", Q_ARG(bool, enabled)); +} + +- (void)notifyStatusChanged:(NSNotification*)notify { + NEVPNConnection* conn = static_cast(notify.object); + QMetaObject::invokeMethod(self.parent, "extStatusChange", Q_ARG(int, conn.status)); +} + +@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 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())); +} 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 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() {}