diff --git a/lib/pal/posix/NetworkInformationImpl.mm b/lib/pal/posix/NetworkInformationImpl.mm index 24c7839eb..943c22667 100644 --- a/lib/pal/posix/NetworkInformationImpl.mm +++ b/lib/pal/posix/NetworkInformationImpl.mm @@ -20,7 +20,7 @@ class NetworkInformation : public NetworkInformationImpl, public std::enable_shared_from_this { - public: + public: /// /// /// @@ -59,21 +59,27 @@ virtual NetworkCost GetNetworkCost() /// /// Setup initial network information and start net monitor if requested. /// This cannot be put in constructor because we need to use shared_from_this. - /// - void SetupNetDetect(); + /// + void SetupNetDetect(); - private: - void UpdateType(NetworkType type) noexcept; - void UpdateCost(NetworkCost cost) noexcept; - std::string m_network_provider {}; + private: + void SetupModernNetDetect() API_AVAILABLE(macos(10.14), ios(12.0)); +#if ODW_LEGACY_REACHABILITY_REQUIRED + void SetupLegacyNetDetect(); +#endif + void UpdateType(NetworkType type) noexcept; + void UpdateCost(NetworkCost cost) noexcept; + std::string m_network_provider {}; - // iOS 12 and newer - nw_path_monitor_t m_monitor = nil; + // iOS 12+ / macOS 10.14+ + nw_path_monitor_t m_monitor = nil; - // iOS 11 and older - ODWReachability* m_reach = nil; - id m_notificationId = nil; - }; +#if ODW_LEGACY_REACHABILITY_REQUIRED + // Older Apple deployment targets still need the legacy fallback. + ODWReachability* m_reach = nil; + id m_notificationId = nil; +#endif + }; NetworkInformation::NetworkInformation(IRuntimeConfig& configuration) : NetworkInformationImpl(configuration) @@ -84,6 +90,7 @@ virtual NetworkCost GetNetworkCost() NetworkInformation::~NetworkInformation() noexcept { +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { if (m_isNetDetectEnabled) @@ -99,108 +106,130 @@ virtual NetworkCost GetNetworkCost() [m_reach stopNotifier]; } } +#else + if (m_isNetDetectEnabled) + { + nw_path_monitor_cancel(m_monitor); + } +#endif } - void NetworkInformation::SetupNetDetect() + void NetworkInformation::SetupModernNetDetect() { - if (@available(macOS 10.14, iOS 12.0, *)) + auto weak_this = std::weak_ptr(shared_from_this()); + + m_monitor = nw_path_monitor_create(); + nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); + nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) { - auto weak_this = std::weak_ptr(shared_from_this()); + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - m_monitor = nw_path_monitor_create(); - nw_path_monitor_set_queue(m_monitor, dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); - nw_path_monitor_set_update_handler(m_monitor, ^(nw_path_t path) + NetworkType type = NetworkType_Unknown; + NetworkCost cost = NetworkCost_Unknown; + nw_path_status_t status = nw_path_get_status(path); + bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; + if (connected) { - auto strong_this = weak_this.lock(); - if (!strong_this) + if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) { - return; + type = NetworkType_Wifi; } - - NetworkType type = NetworkType_Unknown; - NetworkCost cost = NetworkCost_Unknown; - nw_path_status_t status = nw_path_get_status(path); - bool connected = status == nw_path_status_satisfied || status == nw_path_status_satisfiable; - if (connected) + else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) { - if (nw_path_uses_interface_type(path, nw_interface_type_wifi)) - { - type = NetworkType_Wifi; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_cellular)) - { - type = NetworkType_WWAN; - } - else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) - { - type = NetworkType_Wired; - } - cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; - if (@available(macOS 10.15, iOS 13.0, *)) + type = NetworkType_WWAN; + } + else if (nw_path_uses_interface_type(path, nw_interface_type_wired)) + { + type = NetworkType_Wired; + } + cost = nw_path_is_expensive(path) ? NetworkCost_Metered : NetworkCost_Unmetered; + if (@available(macOS 10.15, iOS 13.0, *)) + { + if (nw_path_is_constrained(path)) { - if (nw_path_is_constrained(path)) - { - cost = NetworkCost_Roaming; - } + cost = NetworkCost_Roaming; } } - strong_this->UpdateType(type); - strong_this->UpdateCost(cost); - }); - nw_path_monitor_start(m_monitor); - - // nw_path_monitor_start will invoke the callback for once. So if - // we don't want to listen for changes, we can just start the - // monitor and stop it right away. - if (!m_isNetDetectEnabled) - { - nw_path_monitor_cancel(m_monitor); } - } - else + strong_this->UpdateType(type); + strong_this->UpdateCost(cost); + }); + nw_path_monitor_start(m_monitor); + + // nw_path_monitor_start will invoke the callback for once. So if + // we don't want to listen for changes, we can just start the + // monitor and stop it right away. + if (!m_isNetDetectEnabled) { - auto weak_this = std::weak_ptr(shared_from_this()); + nw_path_monitor_cancel(m_monitor); + } + } - m_reach = [ODWReachability reachabilityForInternetConnection]; - void (^block)(NSNotification*) = ^(NSNotification*) - { - auto strong_this = weak_this.lock(); - if (!strong_this) - { - return; - } +#if ODW_LEGACY_REACHABILITY_REQUIRED + void NetworkInformation::SetupLegacyNetDetect() + { + auto weak_this = std::weak_ptr(shared_from_this()); - // NetworkCost information is not available until iOS 12. - // Just make the best guess here. - switch (m_reach.currentReachabilityStatus) - { - case NotReachable: - strong_this->UpdateType(NetworkType_Unknown); - strong_this->UpdateCost(NetworkCost_Unknown); - break; - case ReachableViaWiFi: - strong_this->UpdateType(NetworkType_Wifi); - strong_this->UpdateCost(NetworkCost_Unmetered); - break; - case ReachableViaWWAN: - strong_this->UpdateType(NetworkType_WWAN); - strong_this->UpdateCost(NetworkCost_Metered); - break; - } - }; - block(nil); // Update the initial status. + m_reach = [ODWReachability reachabilityForInternetConnection]; + void (^block)(NSNotification*) = ^(NSNotification*) + { + auto strong_this = weak_this.lock(); + if (!strong_this) + { + return; + } - if (m_isNetDetectEnabled) + // NetworkCost information is not available until iOS 12. + // Just make the best guess here. + switch (m_reach.currentReachabilityStatus) { - m_notificationId = - [[NSNotificationCenter defaultCenter] - addObserverForName: kNetworkReachabilityChangedNotification - object: nil - queue: nil - usingBlock: block]; - [m_reach startNotifier]; + case NotReachable: + strong_this->UpdateType(NetworkType_Unknown); + strong_this->UpdateCost(NetworkCost_Unknown); + break; + case ReachableViaWiFi: + strong_this->UpdateType(NetworkType_Wifi); + strong_this->UpdateCost(NetworkCost_Unmetered); + break; + case ReachableViaWWAN: + strong_this->UpdateType(NetworkType_WWAN); + strong_this->UpdateCost(NetworkCost_Metered); + break; } + }; + block(nil); // Update the initial status. + + if (m_isNetDetectEnabled) + { + m_notificationId = + [[NSNotificationCenter defaultCenter] + addObserverForName: kNetworkReachabilityChangedNotification + object: nil + queue: nil + usingBlock: block]; + [m_reach startNotifier]; + } + } +#endif + + void NetworkInformation::SetupNetDetect() + { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + SetupModernNetDetect(); + } + else + { + SetupLegacyNetDetect(); } +#else + SetupModernNetDetect(); +#endif } void NetworkInformation::UpdateType(NetworkType type) noexcept @@ -229,4 +258,3 @@ virtual NetworkCost GetNetworkCost() } } PAL_NS_END - diff --git a/third_party/Reachability/ODWReachability.h b/third_party/Reachability/ODWReachability.h index b2d218417..190d4b9d8 100644 --- a/third_party/Reachability/ODWReachability.h +++ b/third_party/Reachability/ODWReachability.h @@ -25,8 +25,10 @@ POSSIBILITY OF SUCH DAMAGE. */ +#import #import #import +#import /** @@ -40,6 +42,22 @@ extern NSString* const kNetworkReachabilityChangedNotification; +// Older Apple deployment targets still need the legacy SCNetworkReachability +// backend at runtime. Newer targets can compile directly to the modern path. +#ifndef TARGET_OS_IOS +#define TARGET_OS_IOS 0 +#endif + +#if TARGET_OS_IOS +#define ODW_LEGACY_REACHABILITY_REQUIRED (__IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) +#elif TARGET_OS_OSX +#define ODW_LEGACY_REACHABILITY_REQUIRED (__MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14) +#else +#define ODW_LEGACY_REACHABILITY_REQUIRED 0 +#endif + +#define ODW_REACHABILITY_HAS_WWAN TARGET_OS_IOS + typedef NS_ENUM(NSInteger, ODWNetworkStatus) { // Apple NetworkStatus Compatible Names. NotReachable = 0, diff --git a/third_party/Reachability/ODWReachability.m b/third_party/Reachability/ODWReachability.m index 7947f7df0..ec50fafa7 100644 --- a/third_party/Reachability/ODWReachability.m +++ b/third_party/Reachability/ODWReachability.m @@ -27,12 +27,30 @@ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #import "ODWReachability.h" +#import + #import #import #import NSString *const kNetworkReachabilityChangedNotification = @"NetworkReachabilityChangedNotification"; +static char ODWReachabilityQueueKey; + +@class ODWReachability; + +@interface ODWReachabilityMonitorContext : NSObject + +#if __has_feature(objc_arc) +@property (nonatomic, weak) ODWReachability *owner; +#else +@property (nonatomic, unsafe_unretained) ODWReachability *owner; +#endif + +@end + +@implementation ODWReachabilityMonitorContext +@end @interface ODWReachability () @@ -40,9 +58,24 @@ @interface ODWReachability () @property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; @property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; @property (nonatomic, strong) id reachabilityObject; +@property (nonatomic, strong) nw_path_monitor_t pathMonitor; +@property (nonatomic, strong) ODWReachabilityMonitorContext *pathMonitorContext; +@property (nonatomic, strong) dispatch_semaphore_t initialPathSemaphore; +@property (nonatomic, assign) nw_path_status_t currentPathStatus; +@property (nonatomic, assign) BOOL currentPathUsesWiFi; +@property (nonatomic, assign) BOOL currentPathUsesWWAN; +@property (nonatomic, assign) BOOL hasObservedPath; +@property (nonatomic, assign) BOOL monitorLocalWiFiOnly; -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; +-(BOOL)getReachabilityFlags:(SCNetworkReachabilityFlags *)flags; +-(BOOL)startLegacyNotifier; +-(void)stopLegacyNotifier; +-(BOOL)ensureModernPathMonitor API_AVAILABLE(macos(10.14), ios(12.0)); +-(BOOL)awaitModernPathSnapshot API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)handleModernPathUpdate:(nw_path_t)path API_AVAILABLE(macos(10.14), ios(12.0)); +-(void)notifyModernPathChange API_AVAILABLE(macos(10.14), ios(12.0)); @end @@ -50,7 +83,7 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) { return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', #else 'X', @@ -62,7 +95,12 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', - (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; + (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; +} + +static BOOL ODWModernPathIsReachable(nw_path_status_t status) API_AVAILABLE(macos(10.14), ios(12.0)) +{ + return status == nw_path_status_satisfied || status == nw_path_status_satisfiable; } // Start listening for reachability notifications on the current run loop @@ -89,84 +127,140 @@ @implementation ODWReachability +(ODWReachability*)reachabilityWithHostName:(NSString*)hostname { - if (hostname == nil || [hostname length] == 0) - { - NSLog(@"Invalid hostname"); - return nil; - } return [ODWReachability reachabilityWithHostname:hostname]; } +(instancetype)reachabilityWithHostname:(NSString*)hostname { - if (@available(macOS 10.14, iOS 12.0, *)) + if (hostname == nil || [hostname length] == 0) { - // Use URLSession for macOS 10.14 or higher - NSString *formattedHostname = hostname; - if (![formattedHostname hasPrefix:@"https://"] && ![formattedHostname hasPrefix:@"http://"]) { - formattedHostname = [NSString stringWithFormat:@"https://%@", hostname]; + NSLog(@"Invalid hostname '%@': hostname is empty", hostname); + return nil; + } + + NSString *reachabilityHost = hostname; + NSURL *url = nil; + NSURLComponents *components = [NSURLComponents componentsWithString:hostname]; + if ([components.scheme length] > 0) + { + if ([components.host length] == 0) + { + NSLog(@"Invalid hostname '%@': URL has no host", hostname); + return nil; } - NSURL *url = [NSURL URLWithString:formattedHostname]; - - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; - reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; - return reachabilityInstance; + + reachabilityHost = components.host; + url = components.URL; + } + + if (url == nil) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", reachabilityHost]]; } - // Use SCNetworkReachability for macOS 10.14 or lower + // NWPathMonitor has no public hostname-targeted API: it monitors the system + // network path, not per-host reachability. Hostname-based reachability still + // routes through SCNetworkReachabilityCreateWithName, with the deprecated-API + // warning locally suppressed. The modern path-monitor backend is used by the + // hostname-agnostic isReachable* methods below. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [reachabilityHost UTF8String]); #pragma clang diagnostic pop if (ref) { - id reachability = [[self alloc] initWithReachabilityRef:ref]; + ODWReachability *reachability = [[self alloc] initWithReachabilityRef:ref]; + reachability.url = url; return reachability; } + const char *errorString = SCErrorString(SCError()); + NSLog(@"Invalid hostname '%@': SCNetworkReachabilityCreateWithName failed for '%@' (%s)", + hostname, + reachabilityHost, + errorString != NULL ? errorString : "unknown error"); return nil; } +(ODWReachability *)reachabilityWithAddress:(void *)hostAddress { - if (hostAddress == NULL) { - NSLog(@"Invalid address"); + if (hostAddress == NULL) + { + NSLog(@"Invalid address: address pointer is null"); return nil; } - if (@available(macOS 10.14, iOS 12.0, *)) + struct sockaddr_storage addressStorage; + bzero(&addressStorage, sizeof(addressStorage)); + struct sockaddr *address = (struct sockaddr *)hostAddress; + NSURL *url = nil; + if (address->sa_family == AF_INET) { - // Use URLSession for macOS 10.14 or higher - NSString *addressString = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)hostAddress)->sin_addr)]; - NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@", addressString]]; - NSURLSession *session = [NSURLSession sharedSession]; - __block ODWReachability *reachabilityInstance = [[self alloc] init]; - reachabilityInstance.url = url; - NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - reachabilityInstance = [self handleReachabilityResponse:response error:error url:reachabilityInstance.url]; - }]; - [dataTask resume]; - return reachabilityInstance; // Return the instance after resuming the data task + char addressString[INET_ADDRSTRLEN] = { 0 }; + struct sockaddr_in *ipv4Address = (struct sockaddr_in *)&addressStorage; + *ipv4Address = *(struct sockaddr_in *)hostAddress; + if (ipv4Address->sin_len == 0) + { + ipv4Address->sin_len = sizeof(*ipv4Address); + } + // Reject the unspecified IPv4 wildcard (INADDR_ANY / 0.0.0.0). It is not a + // routable host address; the SDK only uses it internally as the legacy + // "internet anywhere" SC probe, which goes through a private path that + // bypasses this validator. + if (ipv4Address->sin_addr.s_addr == htonl(INADDR_ANY)) + { + NSLog(@"Invalid address: IPv4 unspecified address (0.0.0.0) is not a valid host"); + return nil; + } + address = (struct sockaddr *)ipv4Address; + if (inet_ntop(AF_INET, &ipv4Address->sin_addr, addressString, sizeof(addressString)) != NULL) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://%s", addressString]]; + } } - - // Use SCNetworkReachability for macOS 10.14 or lower + else if (address->sa_family == AF_INET6) + { + char addressString[INET6_ADDRSTRLEN] = { 0 }; + struct sockaddr_in6 *ipv6Address = (struct sockaddr_in6 *)&addressStorage; + *ipv6Address = *(struct sockaddr_in6 *)hostAddress; + if (ipv6Address->sin6_len == 0) + { + ipv6Address->sin6_len = sizeof(*ipv6Address); + } + // Reject the unspecified IPv6 wildcard (in6addr_any / ::), same reasoning as IPv4. + if (memcmp(&ipv6Address->sin6_addr, &in6addr_any, sizeof(struct in6_addr)) == 0) + { + NSLog(@"Invalid address: IPv6 unspecified address (::) is not a valid host"); + return nil; + } + address = (struct sockaddr *)ipv6Address; + if (inet_ntop(AF_INET6, &ipv6Address->sin6_addr, addressString, sizeof(addressString)) != NULL) + { + url = [NSURL URLWithString:[NSString stringWithFormat:@"https://[%s]", addressString]]; + } + } + else + { + NSLog(@"Invalid address: unsupported sa_family %d (expected AF_INET or AF_INET6)", address->sa_family); + return nil; + } + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)address); #pragma clang diagnostic pop if (ref) { - id reachability = [[self alloc] initWithReachabilityRef:ref]; + ODWReachability *reachability = [[self alloc] initWithReachabilityRef:ref]; + reachability.url = url; return reachability; } + const char *errorString = SCErrorString(SCError()); + NSLog(@"Invalid address: SCNetworkReachabilityCreateWithAddress failed (%s)", + errorString != NULL ? errorString : "unknown error"); return nil; } @@ -199,16 +293,51 @@ +(ODWReachability *)handleReachabilityResponse:(NSURLResponse *)response error:( +(ODWReachability *)reachabilityForInternetConnection { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + return [[self alloc] init]; + } +#else + return [[self alloc] init]; +#endif + + // Legacy SC fallback. Apple's reference Reachability uses the zero IPv4 + // address (INADDR_ANY) here as a "probe any internet" sentinel — the public + // +reachabilityWithAddress: now rejects that wildcard, so create the SC ref + // directly and bypass the validator. struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); zeroAddress.sin_family = AF_INET; - return [self reachabilityWithAddress:&zeroAddress]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress( + kCFAllocatorDefault, (const struct sockaddr*)&zeroAddress); +#pragma clang diagnostic pop + if (ref) + { + return [[self alloc] initWithReachabilityRef:ref]; + } + return nil; } +(ODWReachability*)reachabilityForLocalWiFi { +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { + ODWReachability *reachability = [[self alloc] init]; + reachability.monitorLocalWiFiOnly = YES; + return reachability; + } +#else + ODWReachability *reachability = [[self alloc] init]; + reachability.monitorLocalWiFiOnly = YES; + return reachability; +#endif + struct sockaddr_in localWifiAddress; bzero(&localWifiAddress, sizeof(localWifiAddress)); localWifiAddress.sin_len = sizeof(localWifiAddress); @@ -222,23 +351,166 @@ +(ODWReachability*)reachabilityForLocalWiFi // Initialization methods --(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +-(instancetype)init { self = [super init]; if (self != nil) { self.reachableOnWWAN = YES; - self.reachabilityRef = ref; + self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); + dispatch_queue_set_specific(self.reachabilitySerialQueue, + &ODWReachabilityQueueKey, + &ODWReachabilityQueueKey, + NULL); + } - // We need to create a serial queue. - // We allocate this once for the lifetime of the notifier. + return self; +} - self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); +-(ODWReachability *)initWithReachabilityRef:(SCNetworkReachabilityRef)ref +{ + self = [self init]; + if (self != nil) + { + self.reachabilityRef = ref; } return self; } +-(BOOL)ensureModernPathMonitor +{ + if (self.pathMonitor != nil) + { + return YES; + } + + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; + self.initialPathSemaphore = dispatch_semaphore_create(0); + self.pathMonitor = self.monitorLocalWiFiOnly + ? nw_path_monitor_create_with_type(nw_interface_type_wifi) + : nw_path_monitor_create(); + + if (self.pathMonitor == nil) + { + return NO; + } + + ODWReachabilityMonitorContext *context = [[ODWReachabilityMonitorContext alloc] init]; + context.owner = self; + self.pathMonitorContext = context; +#if !__has_feature(objc_arc) + [context release]; +#endif + + nw_path_monitor_set_queue(self.pathMonitor, self.reachabilitySerialQueue); + nw_path_monitor_set_update_handler(self.pathMonitor, ^(nw_path_t path) { + ODWReachability *owner = context.owner; + if (owner == nil) + { + return; + } + + [owner handleModernPathUpdate:path]; + }); + nw_path_monitor_start(self.pathMonitor); + + return YES; +} + +-(BOOL)awaitModernPathSnapshot +{ + if (![self ensureModernPathMonitor]) + { + return NO; + } + + if (self.hasObservedPath) + { + return YES; + } + + // Capture the semaphore into a local so a concurrent -stopNotifier on + // another thread cannot release the property between the nil-check and + // the wait below. + dispatch_semaphore_t semaphore = self.initialPathSemaphore; + if (semaphore == nil) + { + return NO; + } + + // Avoid blocking reachability queries on the main thread before the first + // NWPathMonitor update arrives. Callers get a conservative "unknown yet" + // result until the async update handler records the first snapshot. + if ([NSThread isMainThread]) + { + return NO; + } + // The update handler runs on this serial queue, so waiting here would deadlock it. + if (dispatch_get_specific(&ODWReachabilityQueueKey) == &ODWReachabilityQueueKey) + { + return NO; + } + + long waitResult = dispatch_semaphore_wait( + semaphore, + dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); + return waitResult == 0 && self.hasObservedPath; +} + +-(void)handleModernPathUpdate:(nw_path_t)path +{ + self.currentPathStatus = nw_path_get_status(path); + self.currentPathUsesWiFi = nw_path_uses_interface_type(path, nw_interface_type_wifi); +#if ODW_REACHABILITY_HAS_WWAN + self.currentPathUsesWWAN = nw_path_uses_interface_type(path, nw_interface_type_cellular); +#else + self.currentPathUsesWWAN = NO; +#endif + + BOOL firstPath = !self.hasObservedPath; + self.hasObservedPath = YES; + if (firstPath) + { + // Capture the semaphore into a local so a concurrent -stopNotifier + // on another thread cannot release the property between the + // nil-check and the signal below. + dispatch_semaphore_t semaphore = self.initialPathSemaphore; + if (semaphore != nil) + { + dispatch_semaphore_signal(semaphore); + } + } + + if (self.reachabilityObject == self) + { + [self notifyModernPathChange]; + } +} + +-(void)notifyModernPathChange +{ + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + if (self.reachableBlock) + { + self.reachableBlock(self); + } + } + else if (self.unreachableBlock) + { + self.unreachableBlock(self); + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification + object:self]; + }); +} + +(void)setTimeoutDurationInSeconds:(int)timeoutDuration { if (timeoutDuration >= kTimeoutDurationInSeconds) @@ -284,30 +556,36 @@ -(void)dealloc -(BOOL)startNotifier { + if (self.reachabilityRef != nil) + { + return [self startLegacyNotifier]; + } + +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { - // Use URLSession for macOS 10.14 or higher - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error) { - NSLog(@"URLSession failed: %@", error.localizedDescription); - self.reachabilityObject = nil; - } else { - self.reachabilityObject = self; - [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkReachabilityChangedNotification object:self]; +#endif + // Use NWPathMonitor for macOS 10.14 or higher. + if ([self ensureModernPathMonitor]) + { + self.reachabilityObject = self; + if ([self awaitModernPathSnapshot]) + { + [self notifyModernPathChange]; } - }]; - if (task) { - [task resume]; return YES; - } else { - NSLog(@"Failed to create URLSessionDataTask"); - return NO; } + return NO; +#if ODW_LEGACY_REACHABILITY_REQUIRED } - - // Use SCNetworkReachability for macOS 10.14 or lower - // allow start notifier to be called multiple times + + return NO; +#endif +} + +-(BOOL)startLegacyNotifier +{ + // Allow start notifier to be called multiple times. if (self.reachabilityObject && (self.reachabilityObject == self)) { return YES; @@ -317,10 +595,12 @@ -(BOOL)startNotifier context.info = (__bridge void *)self; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) - { - if (SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue)) + BOOL callbackSet = SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context); + BOOL queueSet = callbackSet && SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue); #pragma clang diagnostic pop + if (callbackSet) + { + if (queueSet) { self.reachabilityObject = self; return YES; @@ -350,13 +630,40 @@ -(BOOL)startNotifier -(void)stopNotifier { + if (self.reachabilityRef != nil) + { + [self stopLegacyNotifier]; + return; + } + +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { - // Use URLSession for macOS 10.14 or higher, no specific action is needed for URLSession +#endif + // Use NWPathMonitor for macOS 10.14 or higher. self.reachabilityObject = nil; + if (self.pathMonitor != nil) + { + self.pathMonitorContext.owner = nil; + nw_path_monitor_cancel(self.pathMonitor); + self.pathMonitor = nil; + } + self.pathMonitorContext = nil; + self.initialPathSemaphore = nil; + self.hasObservedPath = NO; + self.currentPathStatus = nw_path_status_invalid; + self.currentPathUsesWiFi = NO; + self.currentPathUsesWWAN = NO; +#if ODW_LEGACY_REACHABILITY_REQUIRED + return; } - // Use SCNetworkReachability for macOS 10.14 or lower + return; +#endif +} + +-(void)stopLegacyNotifier +{ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" // First stop, any callbacks! @@ -391,7 +698,7 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags if( (flags & testcase) == testcase ) connectionUP = NO; -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN if(flags & kSCNetworkReachabilityFlagsIsWWAN) { // We're on 3G. @@ -406,79 +713,111 @@ -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags return connectionUP; } --(BOOL)isReachable +-(BOOL)getReachabilityFlags:(SCNetworkReachabilityFlags *)flags { - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef == nil) { - return [self checkNetworkReachability:true]; + return NO; } - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; - #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) - return NO; + BOOL result = SCNetworkReachabilityGetFlags(self.reachabilityRef, flags); #pragma clang diagnostic pop - - return [self isReachableWithFlags:flags]; + return result; } - --(BOOL)isReachableViaWWAN +-(BOOL)isReachable { -#if TARGET_OS_IPHONE - - SCNetworkReachabilityFlags flags = 0; - - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) + if (self.reachabilityRef != nil) { - // Check we're REACHABLE - if(flags & kSCNetworkReachabilityFlagsReachable) - { - // Now, check we're on WWAN - if(flags & kSCNetworkReachabilityFlagsIsWWAN) - { - return YES; - } - } + SCNetworkReachabilityFlags flags; + return [self getReachabilityFlags:&flags] && [self isReachableWithFlags:flags]; } + +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { #endif + return [self awaitModernPathSnapshot] && ODWModernPathIsReachable(self.currentPathStatus); +#if ODW_LEGACY_REACHABILITY_REQUIRED + } return NO; +#endif } --(BOOL)isReachableViaWiFi + +-(BOOL)isReachableViaWWAN { +#if ODW_REACHABILITY_HAS_WWAN + if (self.reachabilityRef != nil) + { + SCNetworkReachabilityFlags flags = 0; + return [self getReachabilityFlags:&flags] && + (flags & kSCNetworkReachabilityFlagsReachable) && + (flags & kSCNetworkReachabilityFlagsIsWWAN); + } + +#if ODW_LEGACY_REACHABILITY_REQUIRED if (@available(macOS 10.14, iOS 12.0, *)) { - return [self checkNetworkReachability:true]; +#endif + return [self awaitModernPathSnapshot] && + ODWModernPathIsReachable(self.currentPathStatus) && + self.currentPathUsesWWAN; +#if ODW_LEGACY_REACHABILITY_REQUIRED } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags = 0; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop + return NO; +#endif +#else + return NO; +#endif +} + +-(BOOL)isReachableViaWiFi +{ + if (self.reachabilityRef != nil) { - // Check we're reachable - if((flags & kSCNetworkReachabilityFlagsReachable)) + SCNetworkReachabilityFlags flags = 0; + if ([self getReachabilityFlags:&flags] && (flags & kSCNetworkReachabilityFlagsReachable)) { -#if TARGET_OS_IPHONE - // Check we're NOT on WWAN - if((flags & kSCNetworkReachabilityFlagsIsWWAN)) +#if ODW_REACHABILITY_HAS_WWAN + if (flags & kSCNetworkReachabilityFlagsIsWWAN) { return NO; } #endif return YES; } + + return NO; + } + +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) + { +#endif + if (![self awaitModernPathSnapshot] || !ODWModernPathIsReachable(self.currentPathStatus)) + { + return NO; + } +#if ODW_REACHABILITY_HAS_WWAN + if (self.monitorLocalWiFiOnly) + { + return self.currentPathUsesWiFi; + } + + return !self.currentPathUsesWWAN; +#else + return self.monitorLocalWiFiOnly ? self.currentPathUsesWiFi : YES; +#endif +#if ODW_LEGACY_REACHABILITY_REQUIRED } return NO; +#endif } @@ -491,42 +830,38 @@ -(BOOL)isConnectionRequired -(BOOL)connectionRequired { - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { - return [self checkNetworkReachability:false]; + SCNetworkReachabilityFlags flags; + return [self getReachabilityFlags:&flags] && + (flags & kSCNetworkReachabilityFlagsConnectionRequired); } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) { - return (flags & kSCNetworkReachabilityFlagsConnectionRequired); +#endif + return [self awaitModernPathSnapshot] && + self.currentPathStatus == nw_path_status_satisfiable; +#if ODW_LEGACY_REACHABILITY_REQUIRED } return NO; +#endif } // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand { - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { - return [self checkNetworkReachability:true]; - } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; + SCNetworkReachabilityFlags flags; + if (![self getReachabilityFlags:&flags]) + { + return NO; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); } @@ -538,19 +873,14 @@ -(BOOL)isConnectionOnDemand // Is user intervention required? -(BOOL)isInterventionRequired { - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { - return [self checkNetworkReachability:false]; - } - - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags; + SCNetworkReachabilityFlags flags; + if (![self getReachabilityFlags:&flags]) + { + return NO; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop - { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & kSCNetworkReachabilityFlagsInterventionRequired)); } @@ -569,7 +899,7 @@ -(ODWNetworkStatus)currentReachabilityStatus if([self isReachableViaWiFi]) return ReachableViaWiFi; -#if TARGET_OS_IPHONE +#if ODW_REACHABILITY_HAS_WWAN return ReachableViaWWAN; #endif } @@ -579,37 +909,42 @@ -(ODWNetworkStatus)currentReachabilityStatus -(SCNetworkReachabilityFlags)reachabilityFlags { - if (@available(macOS 10.14, iOS 12.0, *)) + if (self.reachabilityRef != nil) { - __block SCNetworkReachabilityFlags flags = 0; - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); - - NSURLSession *session = [NSURLSession sharedSession]; - NSURLSessionDataTask *task = [session dataTaskWithURL:[self url] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - if (error == nil && data != nil) { - flags = kSCNetworkReachabilityFlagsReachable; - } - dispatch_semaphore_signal(semaphore); - }]; - - [task resume]; - dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeoutDurationInSeconds * NSEC_PER_SEC)); - - return flags; + SCNetworkReachabilityFlags flags = 0; + return [self getReachabilityFlags:&flags] ? flags : 0; } - // for macOS 10.14 or lower - SCNetworkReachabilityFlags flags = 0; - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) -#pragma clang diagnostic pop +#if ODW_LEGACY_REACHABILITY_REQUIRED + if (@available(macOS 10.14, iOS 12.0, *)) { +#endif + if (![self awaitModernPathSnapshot]) + { + return 0; + } + + SCNetworkReachabilityFlags flags = 0; + if (ODWModernPathIsReachable(self.currentPathStatus)) + { + flags |= kSCNetworkReachabilityFlagsReachable; + } + if (self.currentPathStatus == nw_path_status_satisfiable) + { + flags |= kSCNetworkReachabilityFlagsConnectionRequired; + } +#if ODW_REACHABILITY_HAS_WWAN + if (self.currentPathUsesWWAN) + { + flags |= kSCNetworkReachabilityFlagsIsWWAN; + } +#endif return flags; +#if ODW_LEGACY_REACHABILITY_REQUIRED } return 0; +#endif } -(NSString*)currentReachabilityString