Просмотр исходного кода

Add interface address to network ID

mirokuratczyk 3 лет назад
Родитель
Сommit
861101dc06

+ 6 - 1
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkID.h

@@ -29,7 +29,12 @@ NS_ASSUME_NONNULL_BEGIN
 ///
 /// See network ID requirements here:
 /// https://godoc.org/github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon#NetworkIDGetter
-+ (NSString *)getNetworkID:(NetworkReachability)networkReachability;
+/// @param reachability ReachabilityProtocol implementer used to determine network ID on iOS >=12.
+/// @param currentNetworkStatus Used to determine network ID on iOS <12.
+/// @param outWarn If non-nil, then a non-fatal error occurred while determining the network ID and a valid network ID will still be returned.
++ (NSString *)getNetworkIDWithReachability:(id<ReachabilityProtocol>)reachability
+                   andCurrentNetworkStatus:(NetworkReachability)currentNetworkStatus
+                                   warning:(NSError *_Nullable *_Nonnull)outWarn;
 
 @end
 

+ 60 - 4
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkID.m

@@ -18,22 +18,32 @@
  */
 
 #import "NetworkID.h"
+#import "NetworkInterface.h"
 #import <CoreTelephony/CTTelephonyNetworkInfo.h>
 #import <CoreTelephony/CTCarrier.h>
 #import <SystemConfiguration/CaptiveNetwork.h>
 
+NSString *kNetworkIDUnknown = @"UNKNOWN";
+
 @implementation NetworkID
 
-+ (NSString *)getNetworkID:(NetworkReachability)networkReachability {
+/// Internal helper function. See comment in header for `getNetworkIDWithReachability:andCurrentNetworkStatus:warning:`.
+/// @param networkReachability Network reachability status.
+/// @param defaultActiveInterfaceName Interface associated with the default route on the device.
+/// @param outWarn If non-nil, then a non-fatal error occurred while determining the network ID and a valid network ID will still be returned.
++ (NSString * _Nonnull)getNetworkID:(NetworkReachability)networkReachability
+         defaultActiveInterfaceName:(NSString*)defaultActiveInterfaceName
+                            warning:(NSError *_Nullable *_Nonnull)outWarn {
+    *outWarn = nil;
 
-    NSMutableString *networkID = [NSMutableString stringWithString:@"UNKNOWN"];
+    NSMutableString *networkID = [NSMutableString stringWithString:kNetworkIDUnknown];
     if (networkReachability == NetworkReachabilityReachableViaWiFi) {
         [networkID setString:@"WIFI"];
         NSArray *networkInterfaceNames = (__bridge_transfer id)CNCopySupportedInterfaces();
         for (NSString *networkInterfaceName in networkInterfaceNames) {
             NSDictionary *networkInterfaceInfo = (__bridge_transfer id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)networkInterfaceName);
-            if (networkInterfaceInfo[@"BSSID"]) {
-                [networkID appendFormat:@"-%@", networkInterfaceInfo[@"BSSID"]];
+            if (networkInterfaceInfo[(__bridge NSString*)kCNNetworkInfoKeyBSSID]) {
+                [networkID appendFormat:@"-%@", networkInterfaceInfo[(__bridge NSString*)kCNNetworkInfoKeyBSSID]];
             }
         }
     } else if (networkReachability == NetworkReachabilityReachableViaCellular) {
@@ -47,10 +57,56 @@
         }
     } else if (networkReachability == NetworkReachabilityReachableViaWired) {
         [networkID setString:@"WIRED"];
+        if (defaultActiveInterfaceName != NULL) {
+            NSError *err;
+            NSString *interfaceAddress = [NetworkInterface getInterfaceAddress:defaultActiveInterfaceName
+                                                                         error:&err];
+            if (err != nil) {
+                NSString *localizedDescription =
+                    [NSString stringWithFormat:@"getNetworkID: error getting interface address %@", err.localizedDescription];
+                *outWarn = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: localizedDescription}];
+                return networkID;
+            } else if (interfaceAddress != nil) {
+                [networkID appendFormat:@"-%@", interfaceAddress];
+            }
+        }
     } else if (networkReachability == NetworkReachabilityReachableViaLoopback) {
         [networkID setString:@"LOOPBACK"];
     }
     return networkID;
 }
 
+// See comment in header.
++ (NSString *)getNetworkIDWithReachability:(id<ReachabilityProtocol>)reachability
+                   andCurrentNetworkStatus:(NetworkReachability)currentNetworkStatus
+                                   warning:(NSError *_Nullable *_Nonnull)outWarn {
+    *outWarn = nil;
+
+    NSError *err;
+    NSString *activeInterface =
+        [NetworkInterface getActiveInterfaceWithReachability:reachability
+                                     andCurrentNetworkStatus:currentNetworkStatus
+                                                       error:&err];
+    if (err != nil) {
+        NSString *localizedDescription = [NSString stringWithFormat:@"error getting active interface %@", err.localizedDescription];
+        *outWarn = [[NSError alloc] initWithDomain:@"iOSLibrary"
+                                              code:1
+                                          userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+        return kNetworkIDUnknown;
+    }
+
+    NSError *warn;
+    NSString *networkID = [NetworkID getNetworkID:currentNetworkStatus
+                       defaultActiveInterfaceName:activeInterface
+                                          warning:&warn];
+    if (warn != nil) {
+        NSString *localizedDescription = [NSString stringWithFormat:@"error getting network ID: %@", warn.localizedDescription];
+        *outWarn = [[NSError alloc] initWithDomain:@"iOSLibrary"
+                                              code:1
+                                          userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+    }
+
+    return networkID;
+}
+
 @end

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

@@ -19,6 +19,7 @@
 
 #import <Foundation/Foundation.h>
 #import <Network/path.h>
+#import "ReachabilityProtocol.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -36,8 +37,23 @@ NS_ASSUME_NONNULL_BEGIN
 /// NetworkInterface provides a set of functions for discovering active network interfaces on the device.
 @interface NetworkInterface : NSObject
 
+/// Returns address assigned to the given interface. If the interface has no assigned addresses, or only has a link-local IPv6 address,
+/// then nil is returned.
+/// @param interfaceName Interface name. E.g. "en0".
+/// @param outError If non-nil, then an error occurred while trying determine the interface address.
++ (NSString*_Nullable)getInterfaceAddress:(NSString*_Nonnull)interfaceName
+                                    error:(NSError *_Nullable *_Nonnull)outError;
+
 /// Returns list of active interfaces excluding the loopback interface which support communicating with IPv4, or IPv6, addresses.
-+ (NSSet<NSString*>*_Nullable)activeInterfaces;
++ (NSSet<NSString*>*)activeInterfaces:(NSError *_Nullable *_Nonnull)outError;
+
+/// Returns the active interface name.
+/// @param reachability ReachabilityProtocol implementer used to determine active interface on iOS >=12.
+/// @param currentNetworkStatus Used to determine active interface on iOS <12.
+/// @param outError If non-nil, then an error occurred while determining the active interface.
++ (NSString*)getActiveInterfaceWithReachability:(id<ReachabilityProtocol>)reachability
+                        andCurrentNetworkStatus:(NetworkReachability)currentNetworkStatus
+                                          error:(NSError *_Nullable *_Nonnull)outError;
 
 @end
 

+ 168 - 2
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/NetworkInterface.m

@@ -21,8 +21,11 @@
 
 #import <net/if.h>
 #import <ifaddrs.h>
+#import <netdb.h>
+#import <netinet6/in6.h>
 #import <Network/path.h>
 #import <Network/path_monitor.h>
+#import "DefaultRouteMonitor.h"
 
 @implementation NetworkPathState
 
@@ -30,13 +33,92 @@
 
 @implementation NetworkInterface
 
-+ (NSSet<NSString*>*)activeInterfaces {
++ (NSString*_Nullable)getInterfaceAddress:(NSString*_Nonnull)interfaceName
+                                    error:(NSError *_Nullable *_Nonnull)outError {
+    *outError = nil;
+
+    struct ifaddrs *interfaces;
+    if (getifaddrs(&interfaces) != 0) {
+        NSString *localizedDescription = [NSString stringWithFormat:@"getifaddrs error with errno %d", errno];
+        *outError = [[NSError alloc] initWithDomain:@"iOSLibrary"
+                                               code:1
+                                           userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+        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 *curInterfaceName = [NSString stringWithUTF8String:interface->ifa_name];
+                    if ([interfaceName isEqualToString:curInterfaceName]) {
+
+                        // Ignore IPv6 link-local addresses https://developer.apple.com/forums/thread/128215?answerId=403310022#403310022
+                        // Do not ignore link-local IPv4 addresses because it is possible the interface
+                        // is assigned one manually, or if DHCP fails, etc.
+                        if (interface->ifa_addr->sa_family == AF_INET6) {
+                            struct sockaddr_in6 *sa_in6 = (struct sockaddr_in6*)interface->ifa_addr;
+                            if (sa_in6 != NULL) {
+                                struct in6_addr i_a = sa_in6->sin6_addr;
+                                if (IN6_IS_ADDR_LINKLOCAL(&i_a)) {
+                                    // TODO: consider excluding other IP ranges
+                                    continue;
+                                }
+                            }
+                        }
+
+                        char addr[NI_MAXHOST];
+                        int ret = getnameinfo(interface->ifa_addr,
+                                              (socklen_t)interface->ifa_addr->sa_len,
+                                              addr,
+                                              (socklen_t)NI_MAXHOST,
+                                              nil,
+                                              (socklen_t)0,
+                                              NI_NUMERICHOST);
+                        if (ret != 0) {
+                            NSString *localizedDescription = [NSString stringWithFormat:@"getnameinfo returned %d", ret];
+                            *outError = [[NSError alloc] initWithDomain:@"iOSLibrary"
+                                                                   code:1
+                                                               userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+                            freeifaddrs(interfaces);
+                            return nil;
+                        }
+
+                        freeifaddrs(interfaces);
+
+                        NSString *resolvedAddr = [NSString stringWithUTF8String:addr];
+
+                        return resolvedAddr;
+                    }
+                }
+            }
+        }
+    }
+
+    freeifaddrs(interfaces);
+
+    return nil;
+}
+
++ (NSSet<NSString*>*)activeInterfaces:(NSError *_Nullable *_Nonnull)outError {
+
+    *outError = nil;
 
     NSMutableSet *upIffList = [NSMutableSet new];
 
     struct ifaddrs *interfaces;
     if (getifaddrs(&interfaces) != 0) {
-        return nil;
+        NSString *localizedDescription = [NSString stringWithFormat:@"getifaddrs error with errno %d", errno];
+        *outError = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
+        return upIffList;
     }
 
     struct ifaddrs *interface;
@@ -63,4 +145,88 @@
     return upIffList;
 }
 
+/*!
+ @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 DefaultRouteMonitor instead on iOS 12.0+.
+ */
++ (NSString *)getActiveInterface:(NSSet<NSString*>*)upIffList
+            currentNetworkStatus:(NetworkReachability)currentNetworkStatus {
+
+    // TODO: following is a heuristic for choosing active network interface
+    // Only Wi-Fi and Cellular interfaces are considered
+    // @see : https://forums.developer.apple.com/thread/76711
+    NSArray *iffPriorityList = @[@"en0", @"pdp_ip0"];
+    if (currentNetworkStatus == NetworkReachabilityReachableViaCellular) {
+        iffPriorityList = @[@"pdp_ip0", @"en0"];
+    }
+    for (NSString * key in iffPriorityList) {
+        for (NSString * upIff in upIffList) {
+            if ([key isEqualToString:upIff]) {
+                return [NSString stringWithString:upIff];
+            }
+        }
+    }
+
+    return nil;
+}
+
++ (NSString*)getActiveInterfaceWithReachability:(id<ReachabilityProtocol>)reachability
+                        andCurrentNetworkStatus:(NetworkReachability)currentNetworkStatus
+                                          error:(NSError *_Nullable *_Nonnull)outError {
+
+    *outError = nil;
+
+    NSError *err;
+    NSSet<NSString*>* upIffList = [NetworkInterface activeInterfaces:&err];
+    if (err != nil) {
+        NSString *localizedDescription = [NSString stringWithFormat:@"bindToDevice: error getting active interfaces %@", err.localizedDescription];
+        *outError = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: localizedDescription}];
+        return @"";
+    }
+    if (upIffList == nil) {
+        *outError = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: no active interfaces"}];
+        return @"";
+    }
+
+    NSString *activeInterface;
+
+    if (@available(iOS 12.0, *)) {
+        // 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*)reachability;
+        if (gwMonitor == nil) {
+            *outError = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: DefaultRouteMonitor nil"}];
+            return @"";
+        }
+        NetworkPathState *state = [gwMonitor pathState];
+        if (state == nil) {
+            *outError = [[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 = [NetworkInterface getActiveInterface:upIffList
+                                          currentNetworkStatus:currentNetworkStatus];
+    }
+
+    if (activeInterface == nil) {
+        *outError = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey: @"bindToDevice: no active interface"}];
+        return @"";
+    }
+
+    return activeInterface;
+}
+
 @end

+ 44 - 28
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Network/Reachability/DefaultRouteMonitor.m

@@ -90,6 +90,36 @@
     }
 }
 
+nw_interface_type_t
+nw_path_interface_type(nw_path_t path) API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) {
+    // Discover active interface type. Follows: https://developer.apple.com/forums/thread/105822?answerId=322343022#322343022.
+    if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) {
+        return nw_interface_type_wifi;
+    } else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) {
+        return nw_interface_type_cellular;
+    } else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) {
+        return nw_interface_type_wired;
+    } else if (nw_path_uses_interface_type(path, nw_interface_type_loopback)) {
+        return nw_interface_type_loopback;
+    } else {
+        return nw_interface_type_other;
+    }
+}
+
+NetworkReachability nw_interface_type_network_reachability(nw_interface_type_t interface_type) {
+    if (interface_type == nw_interface_type_wifi) {
+        return NetworkReachabilityReachableViaWiFi;
+    } else if (interface_type == nw_interface_type_cellular) {
+        return NetworkReachabilityReachableViaCellular;
+    } else if (interface_type == nw_interface_type_wired) {
+        return NetworkReachabilityReachableViaWired;
+    } else if (interface_type == nw_interface_type_loopback) {
+        return NetworkReachabilityReachableViaLoopback;
+    } else {
+        return NetworkReachabilityReachableViaUnknown;
+    }
+}
+
 - (void)start API_AVAILABLE(macos(10.14), ios(12.0), watchos(5.0), tvos(12.0)) {
     @synchronized (self) {
         // Ensure previous monitor cancelled
@@ -112,38 +142,23 @@
             }
 
             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) {
+            if (status == nw_path_status_invalid || 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;
-                }
+                // Network is, or could, be reachable. Determine interface corresponding to this
+                // path.
+
+                nw_interface_type_t active_interface_type = nw_path_interface_type(path);
+                self->status = nw_interface_type_network_reachability(active_interface_type);
 
-                NSSet<NSString*>* activeInterfaces = [NetworkInterface activeInterfaces];
+                NSError *err;
+                NSSet<NSString*>* activeInterfaces = [NetworkInterface activeInterfaces:&err];
+                if (err != nil) {
+                    [self log:[NSString stringWithFormat:@"failed to get active interfaces %@", err.localizedDescription]];
+                    // Continue. activeInterfaces will be an empty set (non-nil) and we still want
+                    // to log interfaces enumerated with nw_path_enumerate_interfaces for debugging.
+                }
                 [self log:[NSString stringWithFormat:@"active interfaces %@", activeInterfaces]];
 
                 NSMutableArray<NSString*> *candidateInterfaces = [[NSMutableArray alloc] init];
@@ -167,6 +182,7 @@
                 });
                 [self log:[NSString stringWithFormat:@"%lu candidate interfaces",
                            (unsigned long)[candidateInterfaces count]]];
+
                 if ([candidateInterfaces count] > 0) {
                     // Arbitrarily choose first interface
                     NSString *interfaceName = [candidateInterfaces objectAtIndex:0];

+ 3 - 0
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/Psiphon/PsiphonProviderNetwork.h

@@ -23,6 +23,9 @@
 NS_ASSUME_NONNULL_BEGIN
 
 @interface PsiphonProviderNetwork : NSObject <GoPsiPsiphonProviderNetwork>
+
+- (instancetype)initWithLogger:(void (^__nonnull)(NSString *_Nonnull))logger;
+
 @end
 
 NS_ASSUME_NONNULL_END

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

@@ -27,20 +27,40 @@
 
 @implementation PsiphonProviderNetwork {
     id<ReachabilityProtocol> reachability;
+    void (^logger) (NSString *_Nonnull);
+}
+
+- (void)initialize {
+    if (@available(iOS 12.0, *)) {
+        self->reachability = [[DefaultRouteMonitor alloc] init];
+    } else {
+        self->reachability = [Reachability reachabilityForInternetConnection];
+    }
 }
 
 - (id)init {
     self = [super init];
     if (self) {
-        if (@available(iOS 12.0, *)) {
-            self->reachability = [[DefaultRouteMonitor alloc] init];
-        } else {
-            self->reachability = [Reachability reachabilityForInternetConnection];
-        }
+        [self initialize];
     }
     return self;
 }
 
+- (instancetype)initWithLogger:(void (^__nonnull)(NSString *_Nonnull))logger {
+    self = [super init];
+    if (self) {
+        [self initialize];
+        self->logger = logger;
+    }
+    return self;
+}
+
+- (void)logMessage:(NSString*)notice {
+    if (self->logger != nil) {
+        self->logger(notice);
+    }
+}
+
 - (long)hasNetworkConnectivity {
     return [self->reachability reachabilityStatus] != NetworkReachabilityNotReachable;
 }
@@ -51,7 +71,14 @@
 }
 
 - (NSString *)getNetworkID {
-    return [NetworkID getNetworkID:reachability.reachabilityStatus];
+    NSError *warn;
+    NSString *networkID = [NetworkID getNetworkIDWithReachability:self->reachability
+                                          andCurrentNetworkStatus:self->reachability.reachabilityStatus
+                                                          warning:&warn];
+    if (warn != nil) {
+        [self logMessage:[NSString stringWithFormat:@"error getting network ID: %@", warn.localizedDescription]];
+    }
+    return networkID;
 }
 
 @end

+ 36 - 68
MobileLibrary/iOS/PsiphonTunnel/PsiphonTunnel/PsiphonTunnel.m

@@ -1174,44 +1174,13 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
         return @"";
     }
 
-    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, *)) {
-        // 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"}];
+    NSError *err;
+    NSString *activeInterface = [NetworkInterface getActiveInterfaceWithReachability:self->reachability
+                                                             andCurrentNetworkStatus:atomic_load(&self->currentNetworkStatus)
+                                                                               error:&err];
+    if (err != nil) {
+        NSString *localizedDescription = [NSString stringWithFormat:@"bindToDevice: error getting active interface %@", err.localizedDescription];
+        *error = [[NSError alloc] initWithDomain:@"iOSLibrary" code:1 userInfo:@{NSLocalizedDescriptionKey:localizedDescription}];
         return @"";
     }
 
@@ -1251,34 +1220,6 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
     return [NSString stringWithFormat:@"active interface: %@", activeInterface];
 }
 
-/*!
- @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 DefaultRouteMonitor instead on iOS 12.0+.
- */
-- (NSString *)getActiveInterface:(NSSet<NSString*>*)upIffList {
-    
-    // TODO: following is a heuristic for choosing active network interface
-    // 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) == NetworkReachabilityReachableViaCellular) {
-        iffPriorityList = @[@"pdp_ip0", @"en0"];
-    }
-    for (NSString * key in iffPriorityList) {
-        for (NSString * upIff in upIffList) {
-            if ([key isEqualToString:upIff]) {
-                return [NSString stringWithString:upIff];
-            }
-        }
-    }
-    
-    [self logMessage:@"getActiveInterface: No active interface found."];
-    
-    return nil;
-}
-
 - (NSString *)getPrimaryDnsServer {
     // This function is only called when BindToDevice is used/supported.
     // TODO: Implement correctly
@@ -1319,7 +1260,14 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
 }
 
 - (NSString *)getNetworkID {
-    return [NetworkID getNetworkID:[self->reachability reachabilityStatus]];
+    NSError *warn;
+    NSString *networkID = [NetworkID getNetworkIDWithReachability:self->reachability
+                                          andCurrentNetworkStatus:atomic_load(&self->currentNetworkStatus)
+                                                          warning:&warn];
+    if (warn != nil) {
+        [self logMessage:[NSString stringWithFormat:@"error getting network ID: %@", warn.localizedDescription]];
+    }
+    return networkID;
 }
 
 - (void)notice:(NSString *)noticeJSON {
@@ -1763,10 +1711,30 @@ typedef NS_ERROR_ENUM(PsiphonTunnelErrorDomain, PsiphonTunnelErrorCode) {
             }
         };
 
+        NSDateFormatter *rfc3339Formatter = [PsiphonTunnel rfc3339Formatter];
+
+        void (^logger)(NSString * _Nonnull) = ^void(NSString * _Nonnull msg) {
+            __strong PsiphonTunnelFeedback *strongSelf = weakSelf;
+            if (strongSelf == nil) {
+                return;
+            }
+            __strong id<PsiphonTunnelLoggerDelegate> strongLogger = weakLogger;
+            if (strongLogger == nil) {
+                return;
+            }
+            if ([strongLogger respondsToSelector:@selector(onDiagnosticMessage:withTimestamp:)]) {
+
+                NSString *timestampStr = [rfc3339Formatter stringFromDate:[NSDate date]];
+                dispatch_sync(strongSelf->callbackQueue, ^{
+                    [strongLogger onDiagnosticMessage:msg withTimestamp:timestampStr];
+                });
+            }
+        };
+
         PsiphonProviderNoticeHandlerShim *noticeHandler =
             [[PsiphonProviderNoticeHandlerShim alloc] initWithLogger:logNotice];
 
-        PsiphonProviderNetwork *networkInfoProvider = [[PsiphonProviderNetwork alloc] init];
+        PsiphonProviderNetwork *networkInfoProvider = [[PsiphonProviderNetwork alloc] initWithLogger:logger];
 
         GoPsiStartSendFeedback(psiphonConfig, feedbackJson, uploadPath,
                                innerFeedbackHandler, networkInfoProvider, noticeHandler,