Browse Source

Use NWPathMonitor for reachability info on iOS12+

mirokuratczyk 4 years ago
parent
commit
c04db12120

+ 20 - 8
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel.xcodeproj/project.pbxproj

@@ -43,8 +43,11 @@
 		66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BDB0591DC26CCC0079384C /* SBJson4Writer.m */; };
 		CE3D1DA523906003009A4AF6 /* Backups.h in Headers */ = {isa = PBXBuildFile; fileRef = CE3D1DA323906003009A4AF6 /* Backups.h */; };
 		CE3D1DA623906003009A4AF6 /* Backups.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3D1DA423906003009A4AF6 /* Backups.m */; };
-		CE4616BF2539493600D1243E /* Reachability+HasNetworkConnectivity.h in Headers */ = {isa = PBXBuildFile; fileRef = CE4616BD2539493600D1243E /* Reachability+HasNetworkConnectivity.h */; };
-		CE4616C02539493600D1243E /* Reachability+HasNetworkConnectivity.m in Sources */ = {isa = PBXBuildFile; fileRef = CE4616BE2539493600D1243E /* Reachability+HasNetworkConnectivity.m */; };
+		CE676F2525FA818200F7DB13 /* ReachabilityProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CE676F2325FA818200F7DB13 /* ReachabilityProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
+		CE676F2A25FA82DA00F7DB13 /* DefaultRouteMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = CE676F2825FA82DA00F7DB13 /* DefaultRouteMonitor.h */; };
+		CE676F2B25FA82DA00F7DB13 /* DefaultRouteMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = CE676F2925FA82DA00F7DB13 /* DefaultRouteMonitor.m */; };
+		CE676F2F25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CE676F2D25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.h */; };
+		CE676F3025FA830C00F7DB13 /* Reachability+ReachabilityProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = CE676F2E25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.m */; };
 		CE9549F525C8AAEE00F9AF86 /* Redactor.h in Headers */ = {isa = PBXBuildFile; fileRef = CE9549F325C8AAEE00F9AF86 /* Redactor.h */; };
 		CE9549F625C8AAEE00F9AF86 /* Redactor.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9549F425C8AAEE00F9AF86 /* Redactor.m */; };
 		CEC229FC24F047E700534D04 /* PsiphonProviderNoticeHandlerShim.h in Headers */ = {isa = PBXBuildFile; fileRef = CEC229FA24F047E700534D04 /* PsiphonProviderNoticeHandlerShim.h */; };
@@ -126,8 +129,11 @@
 		66BDB0591DC26CCC0079384C /* SBJson4Writer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBJson4Writer.m; sourceTree = "<group>"; };
 		CE3D1DA323906003009A4AF6 /* Backups.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Backups.h; sourceTree = "<group>"; };
 		CE3D1DA423906003009A4AF6 /* Backups.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Backups.m; sourceTree = "<group>"; };
-		CE4616BD2539493600D1243E /* Reachability+HasNetworkConnectivity.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Reachability+HasNetworkConnectivity.h"; sourceTree = "<group>"; };
-		CE4616BE2539493600D1243E /* Reachability+HasNetworkConnectivity.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Reachability+HasNetworkConnectivity.m"; sourceTree = "<group>"; };
+		CE676F2325FA818200F7DB13 /* ReachabilityProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReachabilityProtocol.h; sourceTree = "<group>"; };
+		CE676F2825FA82DA00F7DB13 /* DefaultRouteMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DefaultRouteMonitor.h; sourceTree = "<group>"; };
+		CE676F2925FA82DA00F7DB13 /* DefaultRouteMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DefaultRouteMonitor.m; sourceTree = "<group>"; };
+		CE676F2D25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Reachability+ReachabilityProtocol.h"; sourceTree = "<group>"; };
+		CE676F2E25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "Reachability+ReachabilityProtocol.m"; sourceTree = "<group>"; };
 		CE9549F325C8AAEE00F9AF86 /* Redactor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Redactor.h; sourceTree = "<group>"; };
 		CE9549F425C8AAEE00F9AF86 /* Redactor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Redactor.m; sourceTree = "<group>"; };
 		CEC229FA24F047E700534D04 /* PsiphonProviderNoticeHandlerShim.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PsiphonProviderNoticeHandlerShim.h; sourceTree = "<group>"; };
@@ -180,8 +186,6 @@
 				CECF014F2538E14B00CD3E5C /* NetworkID.m */,
 				CEDBA51025B7737C007685E2 /* NetworkInterface.h */,
 				CEDBA51125B7737C007685E2 /* NetworkInterface.m */,
-				CE4616BD2539493600D1243E /* Reachability+HasNetworkConnectivity.h */,
-				CE4616BE2539493600D1243E /* Reachability+HasNetworkConnectivity.m */,
 			);
 			path = Network;
 			sourceTree = "<group>";
@@ -302,6 +306,11 @@
 			children = (
 				662659251DD270E900872F6C /* Reachability.h */,
 				662659261DD270E900872F6C /* Reachability.m */,
+				CE676F2325FA818200F7DB13 /* ReachabilityProtocol.h */,
+				CE676F2825FA82DA00F7DB13 /* DefaultRouteMonitor.h */,
+				CE676F2925FA82DA00F7DB13 /* DefaultRouteMonitor.m */,
+				CE676F2D25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.h */,
+				CE676F2E25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.m */,
 			);
 			path = Reachability;
 			sourceTree = "<group>";
@@ -339,8 +348,9 @@
 				6685BDCB1E2E882800F0E414 /* ref.h in Headers */,
 				66BAD3351E525FBC00CD06DE /* JailbreakCheck.h in Headers */,
 				4E89F7FF1E2ED3CE00005F4C /* LookupIPv6.h in Headers */,
-				CE4616BF2539493600D1243E /* Reachability+HasNetworkConnectivity.h in Headers */,
+				CE676F2F25FA830C00F7DB13 /* Reachability+ReachabilityProtocol.h in Headers */,
 				662659271DD270E900872F6C /* Reachability.h in Headers */,
+				CE676F2A25FA82DA00F7DB13 /* DefaultRouteMonitor.h in Headers */,
 				66BDB05D1DC26CCC0079384C /* SBJson4StreamParser.h in Headers */,
 				6685BDD41E2EBB1000F0E414 /* GoPsi.objc.h in Headers */,
 				52BE676825B8A615002DB553 /* PsiphonClientPlatform.h in Headers */,
@@ -362,6 +372,7 @@
 				66BDB0631DC26CCC0079384C /* SBJson4StreamWriter.h in Headers */,
 				CECF01442538D34100CD3E5C /* IPv6Synthesizer.h in Headers */,
 				66BDB0671DC26CCC0079384C /* SBJson4Writer.h in Headers */,
+				CE676F2525FA818200F7DB13 /* ReachabilityProtocol.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -495,12 +506,13 @@
 				4E89F7FE1E2ED3CE00005F4C /* LookupIPv6.c in Sources */,
 				CECF01512538E14B00CD3E5C /* NetworkID.m in Sources */,
 				66BAD3361E525FBC00CD06DE /* JailbreakCheck.m in Sources */,
+				CE676F2B25FA82DA00F7DB13 /* DefaultRouteMonitor.m in Sources */,
 				CEDBA51325B7737C007685E2 /* NetworkInterface.m in Sources */,
+				CE676F3025FA830C00F7DB13 /* Reachability+ReachabilityProtocol.m in Sources */,
 				66BDB0681DC26CCC0079384C /* SBJson4Writer.m in Sources */,
 				66BDB0621DC26CCC0079384C /* SBJson4StreamTokeniser.m in Sources */,
 				66BDB0441DA6C7DD0079384C /* PsiphonTunnel.m in Sources */,
 				CEC229FD24F047E700534D04 /* PsiphonProviderNoticeHandlerShim.m in Sources */,
-				CE4616C02539493600D1243E /* Reachability+HasNetworkConnectivity.m in Sources */,
 				CECF014A2538DD0B00CD3E5C /* PsiphonProviderNetwork.m in Sources */,
 				662659281DD270E900872F6C /* Reachability.m in Sources */,
 				66BDB0601DC26CCC0079384C /* SBJson4StreamParserState.m in Sources */,

+ 2 - 2
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkID.h

@@ -18,7 +18,7 @@
  */
 
 #import <Foundation/Foundation.h>
-#import "Reachability.h"
+#import "ReachabilityProtocol.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
 ///
 /// See network ID requirements here:
 /// https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter
-+ (NSString *)getNetworkID:(NetworkStatus)networkStatus;
++ (NSString *)getNetworkID:(NetworkReachability)networkReachability;
 
 @end
 

+ 3 - 3
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkID.m

@@ -24,10 +24,10 @@
 
 @implementation NetworkID
 
-+ (NSString *)getNetworkID:(NetworkStatus)networkStatus {
++ (NSString *)getNetworkID:(NetworkReachability)networkReachability {
 
     NSMutableString *networkID = [NSMutableString stringWithString:@"UNKNOWN"];
-    if (networkStatus == ReachableViaWiFi) {
+    if (networkReachability == NetworkReachabilityReachableViaWiFi) {
         [networkID setString:@"WIFI"];
         NSArray *networkInterfaceNames = (__bridge_transfer id)CNCopySupportedInterfaces();
         for (NSString *networkInterfaceName in networkInterfaceNames) {
@@ -36,7 +36,7 @@
                 [networkID appendFormat:@"-%@", networkInterfaceInfo[@"BSSID"]];
             }
         }
-    } else if (networkStatus == ReachableViaWWAN) {
+    } else if (networkReachability == NetworkReachabilityReachableViaCellular) {
         [networkID setString:@"MOBILE"];
         CTTelephonyNetworkInfo *telephonyNetworkinfo = [[CTTelephonyNetworkInfo alloc] init];
         CTCarrier *cellularProvider = [telephonyNetworkinfo subscriberCellularProvider];

+ 1 - 13
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkInterface.h

@@ -29,7 +29,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic, nullable) nw_path_t path;
 
 /// Default active interface available to the network path.
-@property (nonatomic, nullable) nw_interface_t defaultActiveInterface;
+@property (nonatomic, nullable) NSString* defaultActiveInterfaceName;
 
 @end
 
@@ -39,18 +39,6 @@ NS_ASSUME_NONNULL_BEGIN
 /// 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

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

@@ -63,56 +63,4 @@
     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

+ 0 - 28
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability+HasNetworkConnectivity.m

@@ -1,28 +0,0 @@
-/*
- * 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 "Reachability+HasNetworkConnectivity.h"
-
-@implementation Reachability (NetworkConnectivity)
-
-- (long)hasNetworkConnectivity {
-    return [self currentReachabilityStatus] != NotReachable;
-}
-
-@end

+ 65 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/DefaultRouteMonitor.h

@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2021, 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 "ReachabilityProtocol.h"
+#import "NetworkInterface.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// ReachabilityChangedNotification represents the reachability state on the device.
+@interface ReachabilityChangedNotification : NSObject
+
+- (instancetype)initWithReachabilityStatus:(NetworkReachability)networkReachability
+             curDefaultActiveInterfaceName:(NSString*)curDefaultActiveInterfaceName
+            prevDefaultActiveInterfaceName:(NSString*)prevDefaultActiveInterfaceName;
+
+/// Current reachability status.
+@property (nonatomic, readonly) NetworkReachability reachabilityStatus;
+
+/// Name of current default active interface. If nil, then there is no such interface.
+@property (nonatomic, nullable, readonly) NSString* curDefaultActiveInterfaceName;
+
+/// Name of previous default active interface. If nil, then there was no default active interface previously or the previous default active
+/// interface was not capable of sending or receiving network data at the time.
+@property (nonatomic, nullable, readonly) NSString* prevDefaultActiveInterfaceName;
+
+@end
+
+/// DefaultRouteMonitor monitors changes to the default route on the device and whether that route is capable of sending and
+/// receiving network data.
+@interface DefaultRouteMonitor : NSObject <ReachabilityProtocol>
+
+- (instancetype)init API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+
+- (id)initWithLogger:(void (^__nonnull)(NSString *_Nonnull))logger API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+
+/// Returns the state of the default route on the device. If nil, then there is no usable route available for sending or receiving network data.
+- (NetworkPathState*_Nullable)pathState;
+
+// Denote ReachabilityProtocol availability.
+- (BOOL)startNotifier API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+- (void)stopNotifier API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
++ (NSString*)reachabilityChangedNotification;
+- (NetworkReachability)reachabilityStatus API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+- (NSString*)reachabilityStatusDebugInfo API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0));
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 323 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/DefaultRouteMonitor.m

@@ -0,0 +1,323 @@
+/*
+ * Copyright (c) 2021, 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 "DefaultRouteMonitor.h"
+#import "NetworkInterface.h"
+#import <Network/path.h>
+#import <net/if.h>
+#import <ifaddrs.h>
+
+@interface ReachabilityChangedNotification ()
+/// See comment in DefaultRouteMonitor.h
+@property (nonatomic) NetworkReachability reachabilityStatus;
+/// See comment in DefaultRouteMonitor.h
+@property (nonatomic, nullable) NSString *curDefaultActiveInterfaceName;
+/// See comment in DefaultRouteMonitor.h
+@property (nonatomic, nullable) NSString *prevDefaultActiveInterfaceName;
+@end
+
+@implementation ReachabilityChangedNotification
+
+- (instancetype)initWithReachabilityStatus:(NetworkReachability)networkReachability
+             curDefaultActiveInterfaceName:(NSString*)curDefaultActiveInterfaceName
+            prevDefaultActiveInterfaceName:(NSString*)prevDefaultActiveInterfaceName {
+    self = [super init];
+    if (self) {
+        self->_reachabilityStatus = networkReachability;
+        self->_curDefaultActiveInterfaceName = curDefaultActiveInterfaceName;
+        self->_prevDefaultActiveInterfaceName = prevDefaultActiveInterfaceName;
+    }
+    return self;
+}
+
+@end
+
+@implementation DefaultRouteMonitor {
+    nw_path_monitor_t monitor;
+    dispatch_queue_t workQueue;
+    NetworkReachability status;
+    NetworkPathState* state;
+
+    void (^logger) (NSString *_Nonnull);
+}
+
+- (void)initialize {
+    self->state = nil;
+    self->status = NetworkReachabilityNotReachable;
+    self->workQueue = dispatch_queue_create("com.psiphon3.library.NWInterfaceNWPathMonitorQueue", DISPATCH_QUEUE_SERIAL);
+}
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        [self initialize];
+    }
+    return self;
+}
+
+- (instancetype)initWithLogger:(void (^__nonnull)(NSString *_Nonnull))logger {
+    self = [super init];
+    if (self) {
+        [self initialize];
+        self->logger = logger;
+    }
+    return self;
+}
+
+- (NetworkPathState*)pathState {
+    return self->state;
+}
+
+- (void)log:(NSString*)notice {
+    if (self->logger != nil) {
+        self->logger(notice);
+    }
+}
+
+- (void)start API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) {
+    @synchronized (self) {
+        // Ensure previous monitor cancelled
+        if (self->monitor != nil) {
+            nw_path_monitor_cancel(self->monitor);
+        }
+
+        self->monitor = nw_path_monitor_create();
+        nw_path_monitor_set_queue(self->monitor, self->workQueue);
+
+        nw_path_monitor_set_update_handler(self->monitor, ^(nw_path_t  _Nonnull path) {
+            [self log:[NSString stringWithFormat:@"new path: %@",
+                       [DefaultRouteMonitor pathDebugInfo:path]]];
+
+            NetworkPathState *newPathState = [[NetworkPathState alloc] init];
+            newPathState.path = path;
+            NSString *prevDefaultActiveInterfaceName = nil;
+            if (self->state != nil) {
+                prevDefaultActiveInterfaceName = self->state.defaultActiveInterfaceName;
+            }
+
+            nw_path_status_t status = nw_path_get_status(path);
+            if (status == nw_path_status_invalid) {
+                self->status = NetworkReachabilityNotReachable;
+            } else if (status == nw_path_status_unsatisfied) {
+                self->status = NetworkReachabilityNotReachable;
+            } else if (status == nw_path_status_satisfied || status == nw_path_status_satisfiable) {
+                if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) {
+                    self->status = NetworkReachabilityReachableViaWiFi;
+                } else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) {
+                    self->status = NetworkReachabilityReachableViaCellular;
+                } else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) {
+                    self->status = NetworkReachabilityReachableViaWired;
+                } else if (nw_path_uses_interface_type(path, nw_interface_type_loopback)) {
+                    self->status = NetworkReachabilityReachableViaLoopback;
+                } else {
+                    self->status = NetworkReachabilityReachableViaUnknown;
+                }
+
+                // Discover active interface type. Follows: https://developer.apple.com/forums/thread/105822?answerId=322343022#322343022.
+                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;
+                }
+
+                NSSet<NSString*>* activeInterfaces = [NetworkInterface activeInterfaces];
+                [self log:[NSString stringWithFormat:@"active interfaces %@", activeInterfaces]];
+
+                NSMutableArray<NSString*> *candidateInterfaces = [[NSMutableArray alloc] init];
+                // Map the active interface type to the interface itself
+                nw_path_enumerate_interfaces(path, ^bool(nw_interface_t  _Nonnull interface) {
+                    nw_interface_type_t interfaceType = nw_interface_get_type(interface);
+                    [self log:[NSString stringWithFormat:@"enumerated interface %@ with type %d",
+                               [NSString stringWithUTF8String:nw_interface_get_name(interface)], interfaceType]];
+
+                    if (interfaceType == active_interface_type) {
+                        NSString *interfaceName = [NSString stringWithUTF8String:nw_interface_get_name(interface)];
+                        if ([activeInterfaces containsObject:interfaceName]) {
+                            [candidateInterfaces addObject:interfaceName];
+                            // Note: could return false here to end enumeration and choose first
+                            // candidate interface.
+                            return true;
+                        }
+                    }
+                    // Continue searching
+                    return true;
+                });
+                [self log:[NSString stringWithFormat:@"%lu candidate interfaces",
+                           (unsigned long)[candidateInterfaces count]]];
+                if ([candidateInterfaces count] > 0) {
+                    // Arbitrarily choose first interface
+                    NSString *interfaceName = [candidateInterfaces objectAtIndex:0];
+                    newPathState.defaultActiveInterfaceName = interfaceName;
+                    [self log:[NSString stringWithFormat:@"active interface %@", interfaceName]];
+                } else {
+                    // This should never happen
+                }
+            } else {
+                // Unhandled case. Should never happen.
+            }
+            self->state = newPathState;
+
+            // Backwards compatibility with Reachability
+            ReachabilityChangedNotification *notif =
+                [[ReachabilityChangedNotification alloc]
+                 initWithReachabilityStatus:self->status
+                 curDefaultActiveInterfaceName:newPathState.defaultActiveInterfaceName
+                 prevDefaultActiveInterfaceName:prevDefaultActiveInterfaceName];
+            [[NSNotificationCenter defaultCenter]
+             postNotificationName:[DefaultRouteMonitor reachabilityChangedNotification]
+             object:notif];
+        });
+        nw_path_monitor_start(self->monitor);
+    }
+}
+
+- (void)stop API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) {
+    @synchronized (self) {
+        // Note: this monitor cannot be used after being cancelled. Its update handler will not
+        // fire again and cannot be restarted with nw_path_monitor_start. A new monitor must be
+        // created and started.
+        nw_path_monitor_cancel(self->monitor);
+        self->monitor = nil;
+    }
+}
+
++ (NSString*)pathDebugInfo:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) {
+    NSString *constrained = @"UNAVAILABLE";
+    if (@available(iOS 13.0, *)) {
+        constrained = [NSString stringWithFormat:@"%d", nw_path_is_constrained(path)];
+    }
+
+    NSString *unsatisfiedReason = @"UNAVAILABLE";
+    if (@available(iOS 14.2, *)) {
+        nw_path_unsatisfied_reason_t reason = nw_path_get_unsatisfied_reason(path);
+        if (reason == nw_path_unsatisfied_reason_wifi_denied) {
+            unsatisfiedReason = @"WIFI_DENIED";
+        } else if (reason == nw_path_unsatisfied_reason_cellular_denied) {
+            unsatisfiedReason = @"CELLULAR_DENIED";
+        } else if (reason == nw_path_unsatisfied_reason_local_network_denied) {
+            unsatisfiedReason = @"LOCAL_NETWORK_DENIED";
+        } else if (reason == nw_path_unsatisfied_reason_not_available) {
+            unsatisfiedReason = @"NOT_AVAILABLE";
+        } else {
+            unsatisfiedReason = @"UNKNOWN";
+        }
+    }
+
+    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;
+    }
+
+    // Note: could log nw_endpoint_t information with
+    // nw_path_copy_effective_local_endpoint and nw_path_enumerate_gateways
+    // but could contain PII — more investigation required, but logging
+    // endpoint type may be safe.
+    NSString *s = [NSString stringWithFormat:
+                   @"status %@, "
+                   "active_interface_type %@, "
+                   "path_is_expensive %d, "
+                   "path_is_constrained %@, "
+                   "path_has_ipv4 %d, "
+                   "path_has_ipv6 %d, "
+                   "path_has_dns %d, "
+                   "unsatisfied_reason %@",
+                   [DefaultRouteMonitor pathStatusToString:nw_path_get_status(path)],
+                   [DefaultRouteMonitor interfaceTypeToString:active_interface_type],
+                   nw_path_is_expensive(path), constrained, nw_path_has_ipv4(path),
+                   nw_path_has_ipv6(path), nw_path_has_dns(path), unsatisfiedReason];
+    return s;
+}
+
+#pragma mark ReachabilityProtocol
+
++ (NSString*)reachabilityChangedNotification {
+    return @"kNetworkReachabilityChangedNotification";
+}
+
+- (BOOL)startNotifier {
+    [self log:@"starting NWPathMonitor"];
+    [self start];
+    return TRUE;
+}
+
+- (void)stopNotifier {
+    [self log:@"stopping NWPathMonitor"];
+    [self stop];
+}
+
+- (NetworkReachability)reachabilityStatus {
+    return self->status;
+}
+
+- (NSString*)reachabilityStatusDebugInfo {
+    if (self->state == nil) {
+        return @"state nil";
+    }
+    nw_path_t path = self->state.path;
+    return [DefaultRouteMonitor pathDebugInfo:path];
+}
+
+#pragma mark Helpers (private)
+
++ (NSString*)interfaceTypeToString:(nw_interface_type_t)type {
+    if (type == nw_interface_type_wifi) {
+        return @"WIFI";
+    } else if (type == nw_interface_type_cellular) {
+        return @"CELLULAR";
+    } else if (type == nw_interface_type_wired) {
+        return @"WIRED";
+    } else if (type == nw_interface_type_loopback) {
+        return @"LOOPBACK";
+    } else if (type == nw_interface_type_other) {
+        return @"OTHER";
+    } else {
+        return @"UNKNOWN";
+    }
+}
+
++ (NSString*)pathStatusToString:(nw_path_status_t)status {
+    if (status == nw_path_status_satisfied) {
+        return @"SATISFIED";
+    } else if (status == nw_path_status_satisfiable) {
+        return @"SATISFIABLE";
+    } else if (status == nw_path_status_unsatisfied) {
+        return @"UNSATISFIED";
+    } else if (status == nw_path_status_invalid) {
+        return @"INVALID";
+    } else {
+        return @"UNKNOWN";
+    }
+}
+
+@end

+ 4 - 6
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability+HasNetworkConnectivity.h → MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/Reachability+ReachabilityProtocol.h

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020, Psiphon Inc.
+ * Copyright (c) 2021, Psiphon Inc.
  * All rights reserved.
  *
  * This program is free software: you can redistribute it and/or modify
@@ -19,14 +19,12 @@
 
 #import <Foundation/Foundation.h>
 #import "Reachability.h"
+#import "ReachabilityProtocol.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
-@interface Reachability (HasNetworkConnectivity)
-
-/// Returns 1 if there is network connectivity. Otherwise returns 0.
-- (long)hasNetworkConnectivity;
-
+@interface Reachability (ReachabilityProtocol) <ReachabilityProtocol>
 @end
 
 NS_ASSUME_NONNULL_END
+

+ 47 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/Reachability+ReachabilityProtocol.m

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2021, 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 "Reachability+ReachabilityProtocol.h"
+
+@implementation Reachability (ReachabilityProtocol)
+
++ (NSString*)reachabilityChangedNotification {
+    return kReachabilityChangedNotification;
+}
+
+- (NetworkReachability)reachabilityStatus {
+    NetworkStatus status = [self currentReachabilityStatus];
+    switch (status) {
+        case NotReachable:
+            return NetworkReachabilityNotReachable;
+        case ReachableViaWiFi:
+            return NetworkReachabilityReachableViaWiFi;
+        case ReachableViaWWAN:
+            return NetworkReachabilityReachableViaCellular;
+        default:
+            [NSException raise:@"unexpected reachability status" format:@"%ld", (long)status];
+            return NetworkReachabilityNotReachable;
+    }
+}
+
+- (NSString*)reachabilityStatusDebugInfo {
+    return [self currentReachabilityFlagsToString];
+}
+
+@end

+ 59 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/ReachabilityProtocol.h

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021, 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>
+
+typedef enum : NSInteger {
+    NetworkReachabilityNotReachable = 0,
+    NetworkReachabilityReachableViaWiFi,
+    NetworkReachabilityReachableViaCellular,
+    NetworkReachabilityReachableViaWired,
+    NetworkReachabilityReachableViaLoopback,
+    NetworkReachabilityReachableViaUnknown
+} NetworkReachability;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// ReachabilityProtocol is a protocol for monitoring the reachability of a target network destination. For example, a protocol
+/// implementation could provide reachability information for the default gateway over a specific network interface.
+/// @note The purpose of ReachabilityProtocol is to bridge the gap between Apple's old Reachability APIs and the new
+/// NWPathMonitor (iOS 12.0+) with a common interface that allows each to be used interchangeably. Using a common interface
+/// simplifies supporting older clients which cannot target NWPathMonitor until the minimum iOS target is 12.0+, at which point the
+/// code targeting the legacy Reachability APIs can be removed.
+@protocol ReachabilityProtocol <NSObject>
+
+/// Name of reachability notifications emitted from the default notification center. See comment for `startNotifier`.
++ (NSString*)reachabilityChangedNotification;
+
+/// Start listening for reachability changes. A notification with the name returned by `reachabilityChangedNotification` will be emitted
+/// from the default notification center until `stopNotifier` is called.
+- (BOOL)startNotifier;
+
+/// Stop listening for reachability changes.
+- (void)stopNotifier;
+
+/// Return current reachability status.
+- (NetworkReachability)reachabilityStatus;
+
+/// Return debug string which represents the current network state for logging.
+- (NSString*)reachabilityStatusDebugInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 12 - 6
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Psiphon/PsiphonProviderNetwork.m

@@ -18,25 +18,31 @@
  */
 
 #import "PsiphonProviderNetwork.h"
+#import "DefaultRouteMonitor.h"
 #import "IPv6Synthesizer.h"
-#import "Reachability.h"
-#import "Reachability+HasNetworkConnectivity.h"
 #import "NetworkID.h"
+#import "Reachability.h"
+#import "Reachability+ReachabilityProtocol.h"
+#import "ReachabilityProtocol.h"
 
 @implementation PsiphonProviderNetwork {
-    Reachability *reachability;
+    id<ReachabilityProtocol> reachability;
 }
 
 - (id)init {
     self = [super init];
     if (self) {
-        self->reachability = [Reachability reachabilityForInternetConnection];
+        if (@available(iOS 12.0, *)) {
+            self->reachability = [[DefaultRouteMonitor alloc] init];
+        } else {
+            self->reachability = [Reachability reachabilityForInternetConnection];
+        }
     }
     return self;
 }
 
 - (long)hasNetworkConnectivity {
-    return [self->reachability hasNetworkConnectivity];
+    return [self->reachability reachabilityStatus] != NetworkReachabilityNotReachable;
 }
 
 
@@ -45,7 +51,7 @@
 }
 
 - (NSString *)getNetworkID {
-    return [NetworkID getNetworkID:reachability.currentReachabilityStatus];
+    return [NetworkID getNetworkID:reachability.reachabilityStatus];
 }
 
 @end

+ 3 - 2
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.h

@@ -23,6 +23,7 @@
  */
 
 #import "Reachability.h"
+#import "ReachabilityProtocol.h"
 #import "JailbreakCheck.h"
 #import "PsiphonClientPlatform.h"
 
@@ -180,7 +181,7 @@ Called when the device's Internet connection state has changed.
 This may mean that it had connectivity and now doesn't, or went from Wi-Fi to
 WWAN or vice versa or VPN state changed
 */
-- (void)onInternetReachabilityChanged:(Reachability * _Nonnull)currentReachability;
+- (void)onInternetReachabilityChanged:(NetworkReachability)currentReachability;
 
 /*!
  Called when tunnel-core determines which server egress regions are available
@@ -375,7 +376,7 @@ Returns the path where the rotated notices file will be created.
  disconnected state.
  @return The current reachability status.
  */
-- (BOOL)getNetworkReachabilityStatus:(NetworkStatus * _Nonnull)status;
+- (BOOL)getNetworkReachabilityStatus:(NetworkReachability * _Nonnull)status;
 
 /*!
  Provides the port number of the local SOCKS proxy. Only valid when currently connected (will return 0 otherwise).

+ 77 - 28
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -28,7 +28,6 @@
 #import "PsiphonProviderNoticeHandlerShim.h"
 #import "PsiphonProviderNetwork.h"
 #import "PsiphonTunnel.h"
-#import "Reachability+HasNetworkConnectivity.h"
 #import "Backups.h"
 #import "json-framework/SBJson4.h"
 #import "NetworkID.h"
@@ -37,6 +36,9 @@
 #import <netdb.h>
 #import "PsiphonClientPlatform.h"
 #import "Redactor.h"
+#import "ReachabilityProtocol.h"
+#import "Reachability+ReachabilityProtocol.h"
+#import "DefaultRouteMonitor.h"
 
 #define GOOGLE_DNS_1 @"8.8.4.4"
 #define GOOGLE_DNS_2 @"8.8.8.8"
@@ -111,8 +113,8 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     _Atomic NSInteger localSocksProxyPort;
     _Atomic NSInteger localHttpProxyPort;
 
-    Reachability* reachability;
-    _Atomic NetworkStatus currentNetworkStatus;
+    id<ReachabilityProtocol> reachability;
+    _Atomic NetworkReachability currentNetworkStatus;
 
     BOOL tunnelWholeDevice;
     _Atomic BOOL usingNoticeFiles;
@@ -138,8 +140,17 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     atomic_init(&self->connectionState, PsiphonConnectionStateDisconnected);
     atomic_init(&self->localSocksProxyPort, 0);
     atomic_init(&self->localHttpProxyPort, 0);
-    self->reachability = [Reachability reachabilityForInternetConnection];
-    atomic_init(&self->currentNetworkStatus, NotReachable);
+    // reachability for the default route (destination 0.0.0.0/0)
+    if (@available(iOS 12.0, *)) {
+        void (^logNotice)(NSString * _Nonnull) = ^void(NSString * _Nonnull noticeJSON) {
+            // Warning: any logs emitted before tunneledAppDelegate is set will be discarded silently.
+            [self logMessage:[@"DefaultRouteMonitor: " stringByAppendingString:noticeJSON]];
+        };
+        self->reachability = [[DefaultRouteMonitor alloc] initWithLogger:logNotice];
+    } else {
+        self->reachability = [Reachability reachabilityForInternetConnection];
+    }
+    atomic_init(&self->currentNetworkStatus, NetworkReachabilityNotReachable);
     self->tunnelWholeDevice = FALSE;
     atomic_init(&self->usingNoticeFiles, FALSE);
 
@@ -157,7 +168,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     atomic_init(&self->useInitialDNS, [self->initialDNSCache count] > 0);
 
     rfc3339Formatter = [PsiphonTunnel rfc3339Formatter];
-    
+
     return self;
 }
 
@@ -417,7 +428,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     return atomic_load(&self->connectionState);
 }
 
-- (BOOL)getNetworkReachabilityStatus:(NetworkStatus * _Nonnull)status {
+- (BOOL)getNetworkReachabilityStatus:(NetworkReachability * _Nonnull)status {
     PsiphonConnectionState connState = [self getConnectionState];
     if (connState == PsiphonConnectionStateDisconnected) {
         return FALSE;
@@ -1172,21 +1183,38 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     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];
+        // Note: it is hypothetically possible that NWPathMonitor emits a new path after
+        // bindToDevice is called. This creates a race between DefaultRouteMonitor updating its
+        // internal state and bindToDevice retrieving the active interface from that internal state.
+        // Therefore the following sequence of events is possible:
+        // - NWPathMonitor emits path that is satisfied or satisfiable
+        // - GoPsiPsiphonProvider protocol consumer sees there is connectivity and calls bindToDevice
+        // - NWPathMonitor emits path that is unsatisfied or invalid
+        // - bindToDevice either: a) does not observe update and returns previously active
+        //   interface; or b) observes update and cannot find active interface.
+        // In both scenarios the reachability state will change to unreachable and it is up to the
+        // consumer to call bindToDevice again once it becomes reachable again.
+        DefaultRouteMonitor *gwMonitor = (DefaultRouteMonitor*)self->reachability;
+        if (gwMonitor == nil) {
+            *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: DefaultRouteMonitor nil"}];
+            return @"";
         }
+        NetworkPathState *state = [gwMonitor pathState];
+        if (state == nil) {
+            *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: network path state nil"}];
+            return @"";
+        }
+        // Note: could fallback on heuristic for iOS <12.0 if nil
+        activeInterface = state.defaultActiveInterfaceName;
     } else {
         activeInterface = [self getActiveInterface:upIffList];
     }
+
     if (activeInterface == nil) {
         *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: no active interface"}];
         return @"";
     }
-    
+
     unsigned int interfaceIndex = if_nametoindex([activeInterface UTF8String]);
     if (interfaceIndex == 0) {
         *error = [[NSError alloc] initWithDomain:NSPOSIXErrorDomain code:errno userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"bindToDevice: if_nametoindex failed: %d", errno]}];
@@ -1227,7 +1255,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
  @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+.
+ @warning Use DefaultRouteMonitor instead on iOS 12.0+.
  */
 - (NSString *)getActiveInterface:(NSSet<NSString*>*)upIffList {
     
@@ -1235,7 +1263,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     // Only Wi-Fi and Cellular interfaces are considered
     // @see : https://forums.developer.apple.com/thread/76711
     NSArray *iffPriorityList = @[@"en0", @"pdp_ip0"];
-    if (atomic_load(&self->currentNetworkStatus) == ReachableViaWWAN) {
+    if (atomic_load(&self->currentNetworkStatus) == NetworkReachabilityReachableViaCellular) {
         iffPriorityList = @[@"pdp_ip0", @"en0"];
     }
     for (NSString * key in iffPriorityList) {
@@ -1275,7 +1303,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
 
 - (long)hasNetworkConnectivity {
 
-    BOOL hasConnectivity = [self->reachability currentReachabilityStatus] != NotReachable;
+    BOOL hasConnectivity = [self->reachability reachabilityStatus] != NetworkReachabilityNotReachable;
 
     if (!hasConnectivity) {
         // changeConnectionStateTo self-throttles, so even if called multiple
@@ -1291,7 +1319,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
 }
 
 - (NSString *)getNetworkID {
-    return [NetworkID getNetworkID:[self->reachability currentReachabilityStatus]];
+    return [NetworkID getNetworkID:[self->reachability reachabilityStatus]];
 }
 
 - (void)notice:(NSString *)noticeJSON {
@@ -1440,7 +1468,7 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
 // time for the tunnel to notice the network is gone (depending on attempts to
 // use the tunnel).
 - (void)startInternetReachabilityMonitoring {
-    atomic_store(&self->currentNetworkStatus, [self->reachability currentReachabilityStatus]);
+    atomic_store(&self->currentNetworkStatus, [self->reachability reachabilityStatus]);
 
     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(internetReachabilityChanged:) name:kReachabilityChangedNotification object:nil];
     [self->reachability startNotifier];
@@ -1455,22 +1483,43 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     // Invalidate initialDNSCache.
     atomic_store(&self->useInitialDNS, FALSE);
 
-    Reachability* currentReachability = [note object];
+    NetworkReachability networkStatus;
+    NetworkReachability previousNetworkStatus;
+    BOOL interfaceChanged = FALSE;
 
     // Pass current reachability through to the delegate
     // as soon as a network reachability change is detected
+    if (@available(iOS 12.0, *)) {
+        ReachabilityChangedNotification *notif = [note object];
+        networkStatus = notif.reachabilityStatus;
+        if (notif.prevDefaultActiveInterfaceName == nil && notif.curDefaultActiveInterfaceName == nil) {
+            // no interface change
+        } else if (notif.prevDefaultActiveInterfaceName == nil || notif.curDefaultActiveInterfaceName == nil) {
+            // interface appeared or disappeared
+            interfaceChanged = TRUE;
+        } else if (![notif.prevDefaultActiveInterfaceName isEqualToString:notif.curDefaultActiveInterfaceName]) {
+            // active interface changed
+            interfaceChanged = TRUE;
+        }
+    } else {
+        Reachability* currentReachability = [note object];
+        networkStatus = [currentReachability reachabilityStatus];
+    }
+
     if ([self.tunneledAppDelegate respondsToSelector:@selector(onInternetReachabilityChanged:)]) {
         dispatch_sync(self->callbackQueue, ^{
-            [self.tunneledAppDelegate onInternetReachabilityChanged:currentReachability];
+            [self.tunneledAppDelegate onInternetReachabilityChanged:networkStatus];
         });
     }
-    
-    NetworkStatus networkStatus = [currentReachability currentReachabilityStatus];
-    NetworkStatus previousNetworkStatus = atomic_exchange(&self->currentNetworkStatus, networkStatus);
-    
-    // Restart if the state has changed, unless the previous state was NotReachable, because
-    // the tunnel should be waiting for connectivity in that case.
-    if (networkStatus != previousNetworkStatus && previousNetworkStatus != NotReachable) {
+
+    previousNetworkStatus = atomic_exchange(&self->currentNetworkStatus, networkStatus);
+
+    // Restart if the state has changed, unless the previous state was
+    // NetworkReachabilityNotReachable, because the tunnel should be waiting for connectivity in
+    // that case.
+    BOOL restartDueToNetworkStatusChange = networkStatus != previousNetworkStatus && previousNetworkStatus != NetworkReachabilityNotReachable;
+
+    if (restartDueToNetworkStatusChange || interfaceChanged) {
         GoPsiReconnectTunnel();
     }
 }