Browse Source

Merge pull request #583 from mirokuratczyk/arm-macos

Use NWPathMonitor to find active interface
Rod Hynes 5 years ago
parent
commit
e4ac0c9608

+ 12 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj

@@ -51,8 +51,11 @@
 		CECF014A2538DD0B00CD3E5C /* PsiphonProviderNetwork.m in Sources */ = {isa = PBXBuildFile; fileRef = CECF01482538DD0B00CD3E5C /* PsiphonProviderNetwork.m */; };
 		CECF01502538E14B00CD3E5C /* NetworkID.h in Headers */ = {isa = PBXBuildFile; fileRef = CECF014E2538E14B00CD3E5C /* NetworkID.h */; };
 		CECF01512538E14B00CD3E5C /* NetworkID.m in Sources */ = {isa = PBXBuildFile; fileRef = CECF014F2538E14B00CD3E5C /* NetworkID.m */; };
+		CEDBA51225B7737C007685E2 /* NetworkInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = CEDBA51025B7737C007685E2 /* NetworkInterface.h */; };
+		CEDBA51325B7737C007685E2 /* NetworkInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDBA51125B7737C007685E2 /* NetworkInterface.m */; };
 		CEDE547924EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.h in Headers */ = {isa = PBXBuildFile; fileRef = CEDE547724EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.h */; };
 		CEDE547A24EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDE547824EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.m */; };
+		CEFC764225B1F358003A2A52 /* Network.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEFC764125B1F358003A2A52 /* Network.framework */; };
 		EFED7EBF1F587F6E0078980F /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = EFED7EBE1F587F6E0078980F /* libresolv.tbd */; };
 /* End PBXBuildFile section */
 
@@ -127,8 +130,11 @@
 		CECF01482538DD0B00CD3E5C /* PsiphonProviderNetwork.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PsiphonProviderNetwork.m; sourceTree = "<group>"; };
 		CECF014E2538E14B00CD3E5C /* NetworkID.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkID.h; sourceTree = "<group>"; };
 		CECF014F2538E14B00CD3E5C /* NetworkID.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NetworkID.m; sourceTree = "<group>"; };
+		CEDBA51025B7737C007685E2 /* NetworkInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkInterface.h; sourceTree = "<group>"; };
+		CEDBA51125B7737C007685E2 /* NetworkInterface.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NetworkInterface.m; sourceTree = "<group>"; };
 		CEDE547724EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = PsiphonProviderFeedbackHandlerShim.h; path = ../PsiphonProviderFeedbackHandlerShim.h; sourceTree = "<group>"; };
 		CEDE547824EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = PsiphonProviderFeedbackHandlerShim.m; path = ../PsiphonProviderFeedbackHandlerShim.m; sourceTree = "<group>"; };
+		CEFC764125B1F358003A2A52 /* Network.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Network.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/Library/Frameworks/Network.framework; sourceTree = DEVELOPER_DIR; };
 		EFED7EBE1F587F6E0078980F /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; };
 /* End PBXFileReference section */
 
@@ -139,6 +145,7 @@
 			files = (
 				EFED7EBF1F587F6E0078980F /* libresolv.tbd in Frameworks */,
 				660E0B7A1E2D6EB6002BF5D4 /* Psi in Frameworks */,
+				CEFC764225B1F358003A2A52 /* Network.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -163,6 +170,8 @@
 				CECF01432538D34100CD3E5C /* IPv6Synthesizer.m */,
 				CECF014E2538E14B00CD3E5C /* NetworkID.h */,
 				CECF014F2538E14B00CD3E5C /* NetworkID.m */,
+				CEDBA51025B7737C007685E2 /* NetworkInterface.h */,
+				CEDBA51125B7737C007685E2 /* NetworkInterface.m */,
 				CE4616BD2539493600D1243E /* Reachability+HasNetworkConnectivity.h */,
 				CE4616BE2539493600D1243E /* Reachability+HasNetworkConnectivity.m */,
 			);
@@ -301,6 +310,7 @@
 		EFED7EBD1F587F6E0078980F /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CEFC764125B1F358003A2A52 /* Network.framework */,
 				EFED7EBE1F587F6E0078980F /* libresolv.tbd */,
 			);
 			name = Frameworks;
@@ -329,6 +339,7 @@
 				66BDB0651DC26CCC0079384C /* SBJson4StreamWriterState.h in Headers */,
 				CECF01502538E14B00CD3E5C /* NetworkID.h in Headers */,
 				66BDB05B1DC26CCC0079384C /* SBJson4Parser.h in Headers */,
+				CEDBA51225B7737C007685E2 /* NetworkInterface.h in Headers */,
 				CEDE547924EBF5980053566E /* PsiphonProviderFeedbackHandlerShim.h in Headers */,
 				6685BDCD1E2E88A200F0E414 /* Psi-meta.h in Headers */,
 				66BDB05A1DC26CCC0079384C /* SBJson4.h in Headers */,
@@ -469,6 +480,7 @@
 				4E89F7FE1E2ED3CE00005F4C /* LookupIPv6.c in Sources */,
 				CECF01512538E14B00CD3E5C /* NetworkID.m in Sources */,
 				66BAD3361E525FBC00CD06DE /* JailbreakCheck.m in Sources */,
+				CEDBA51325B7737C007685E2 /* NetworkInterface.m in Sources */,
 				66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */,
 				66BDB0621DC26CCC0079384C /* SBJson4StreamTokeniser.m in Sources */,
 				66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */,

+ 56 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkInterface.h

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2020, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+#import <Network/path.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// NetworkPathState represents the state of the network path on the device.
+@interface NetworkPathState : NSObject
+
+/// Network path state.
+@property (nonatomic, nullable) nw_path_t path;
+
+/// Default active interface available to the network path.
+@property (nonatomic, nullable) nw_interface_t defaultActiveInterface;
+
+@end
+
+/// NetworkInterface provides a set of functions for discovering active network interfaces on the device.
+@interface NetworkInterface : NSObject
+
+/// Returns list of active interfaces excluding the loopback interface which support communicating with IPv4, or IPv6, addresses.
++ (NSSet<NSString*>*_Nullable)activeInterfaces;
+
+/// Returns the currrent network path state and default active interface. The default active interface is found by mapping the active
+/// interface type used by the current network path to the first interface available to that path which is of the same type (e.g. WiFi, Cellular,
+/// etc.). This allows for the possibility of returning a non-default active interface in the scenario where there are other active interfaces
+/// which share the same type as the default active interface. This design limitation is present because querying the routing table is not
+/// supported on iOS; therefore we cannot query the routing table for the interface associated with the default route. Fortunately the
+/// selected interface should always be capable of routing traffic to the internet, even if a non-default active interface is chosen.
+/// @param activeInterfaces If non-nil, then only interfaces available to the current network path which are present in this list will
+/// be considered when searching for the default active interface. If nil, then all interfaces available to the current network path will be
+/// searched.
+/// @return The current network path state. See NetworkPathState for further details.
++ (NetworkPathState*)networkPathState:(NSSet<NSString*>*_Nullable)activeInterfaces API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 118 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkInterface.m

@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2020, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#import "NetworkInterface.h"
+
+#import <net/if.h>
+#import <ifaddrs.h>
+#import <Network/path.h>
+#import <Network/path_monitor.h>
+
+@implementation NetworkPathState
+
+@end
+
+@implementation NetworkInterface
+
++ (NSSet<NSString*>*)activeInterfaces {
+
+    NSMutableSet *upIffList = [NSMutableSet new];
+
+    struct ifaddrs *interfaces;
+    if (getifaddrs(&interfaces) != 0) {
+        return nil;
+    }
+
+    struct ifaddrs *interface;
+    for (interface=interfaces; interface; interface=interface->ifa_next) {
+
+        // Only IFF_UP interfaces. Loopback is ignored.
+        if (interface->ifa_flags & IFF_UP && !(interface->ifa_flags & IFF_LOOPBACK)) {
+
+            if (interface->ifa_addr && (interface->ifa_addr->sa_family==AF_INET || interface->ifa_addr->sa_family==AF_INET6)) {
+
+                // ifa_name could be NULL
+                // https://sourceware.org/bugzilla/show_bug.cgi?id=21812
+                if (interface->ifa_name != NULL) {
+                    NSString *interfaceName = [NSString stringWithUTF8String:interface->ifa_name];
+                    [upIffList addObject:interfaceName];
+                }
+            }
+        }
+    }
+
+    // Free getifaddrs data
+    freeifaddrs(interfaces);
+
+    return upIffList;
+}
+
++ (NetworkPathState*)networkPathState:(NSSet<NSString*>*)activeInterfaces {
+
+    __block NetworkPathState *state = [[NetworkPathState alloc] init];
+
+    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
+
+    nw_path_monitor_t monitor = nw_path_monitor_create();
+
+    nw_path_monitor_set_update_handler(monitor, ^(nw_path_t  _Nonnull path) {
+
+        // Discover the active interface type
+
+        nw_interface_type_t active_interface_type = nw_interface_type_other;
+
+        if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) {
+            active_interface_type = nw_interface_type_wifi;
+        } else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) {
+            active_interface_type = nw_interface_type_cellular;
+        } else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) {
+            active_interface_type = nw_interface_type_wired;
+        } else if (nw_path_uses_interface_type(path, nw_interface_type_loopback)) {
+            active_interface_type = nw_interface_type_loopback;
+        } else {
+            active_interface_type = nw_interface_type_other;
+        }
+
+        // Map the active interface type to the interface itself
+        nw_path_enumerate_interfaces(path, ^bool(nw_interface_t  _Nonnull interface) {
+
+            if (nw_interface_get_type(interface) == active_interface_type) {
+                NSString *interfaceName = [NSString stringWithUTF8String:nw_interface_get_name(interface)];
+                if (state.defaultActiveInterface == NULL && (activeInterfaces == nil || [activeInterfaces containsObject:interfaceName])) {
+                    state.defaultActiveInterface = interface;
+                    return false;
+                }
+            }
+
+            // Continue searching
+            return true;
+        });
+
+        dispatch_semaphore_signal(sem);
+    });
+
+    nw_path_monitor_set_queue(monitor, dispatch_queue_create("com.psiphon3.library.NWInterfaceNWPathMonitorQueue", DISPATCH_QUEUE_SERIAL));
+    nw_path_monitor_start(monitor);
+
+    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
+
+    return state;
+}
+
+@end

+ 26 - 35
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -33,7 +33,7 @@
 #import "json-framework/SBJson4.h"
 #import "JailbreakCheck/JailbreakCheck.h"
 #import "NetworkID.h"
-#import <ifaddrs.h>
+#import "NetworkInterface.h"
 #import <resolv.h>
 #import <netdb.h>
 
@@ -1191,10 +1191,28 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
         *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: invalid mode"}];
         return @"";
     }
-    
-    NSString *activeInterface = [self getActiveInterface];
+
+    NSSet<NSString*>* upIffList = NetworkInterface.activeInterfaces;
+    if (upIffList == nil) {
+        *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: no active interfaces"}];
+        return @"";
+    }
+
+    NSString *activeInterface;
+
+    if (@available(iOS 12.0, *)) {
+
+        NetworkPathState *state = [NetworkInterface networkPathState:upIffList];
+
+        if (state.defaultActiveInterface != nil) {
+            const char *interfaceName = nw_interface_get_name(state.defaultActiveInterface);
+            activeInterface = [NSString stringWithUTF8String:interfaceName];
+        }
+    } else {
+        activeInterface = [self getActiveInterface:upIffList];
+    }
     if (activeInterface == nil) {
-        *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: not active interface"}];
+        *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: no active interface"}];
         return @"";
     }
     
@@ -1235,39 +1253,12 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
 }
 
 /*!
- @brief Returns name of active network interface.
+ @brief Returns name of default active network interface from the provided list of active interfaces.
+ @param upIffList List of active network interfaces.
  @return Active interface name, nil otherwise.
+ @warning Use [NetworkInterface networkPathState:] instead on iOS 12+.
  */
-- (NSString *)getActiveInterface {
-    
-    // Getting list of all active interfaces
-    NSMutableArray *upIffList = [NSMutableArray new];
-    
-    struct ifaddrs *interfaces;
-    if (getifaddrs(&interfaces) != 0) {
-        return nil;
-    }
-    
-    struct ifaddrs *interface;
-    for (interface=interfaces; interface; interface=interface->ifa_next) {
-        
-        // Only IFF_UP interfaces. Loopback is ignored.
-        if (interface->ifa_flags & IFF_UP && !(interface->ifa_flags & IFF_LOOPBACK)) {
-            
-            if (interface->ifa_addr && (interface->ifa_addr->sa_family==AF_INET || interface->ifa_addr->sa_family==AF_INET6)) {
-                
-                // ifa_name could be NULL
-                // https://sourceware.org/bugzilla/show_bug.cgi?id=21812
-                if (interface->ifa_name != NULL) {
-                    NSString *interfaceName = [NSString stringWithUTF8String:interface->ifa_name];
-                    [upIffList addObject:interfaceName];
-                }
-            }
-        }
-    }
-    
-    // Free getifaddrs data
-    freeifaddrs(interfaces);
+- (NSString *)getActiveInterface:(NSSet<NSString*>*)upIffList {
     
     // TODO: following is a heuristic for choosing active network interface
     // Only Wi-Fi and Cellular interfaces are considered