1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "net/base/network_change_notifier_mac.h" 6 7#include <netinet/in.h> 8#include <resolv.h> 9 10#include "base/bind.h" 11#include "base/logging.h" 12#include "base/macros.h" 13#include "base/sequenced_task_runner.h" 14#include "base/task/post_task.h" 15#include "base/task/task_traits.h" 16#include "net/dns/dns_config_service.h" 17 18#if defined(OS_IOS) 19#import <CoreTelephony/CTTelephonyNetworkInfo.h> 20#endif 21 22namespace net { 23 24static bool CalculateReachability(SCNetworkConnectionFlags flags) { 25 bool reachable = flags & kSCNetworkFlagsReachable; 26 bool connection_required = flags & kSCNetworkFlagsConnectionRequired; 27 return reachable && !connection_required; 28} 29 30NetworkChangeNotifierMac::NetworkChangeNotifierMac() 31 : NetworkChangeNotifier(NetworkChangeCalculatorParamsMac()), 32 connection_type_(CONNECTION_UNKNOWN), 33 connection_type_initialized_(false), 34 initial_connection_type_cv_(&connection_type_lock_), 35 forwarder_(this) { 36 // Must be initialized after the rest of this object, as it may call back into 37 // SetInitialConnectionType(). 38 config_watcher_ = std::make_unique<NetworkConfigWatcherMac>(&forwarder_); 39} 40 41NetworkChangeNotifierMac::~NetworkChangeNotifierMac() { 42 ClearGlobalPointer(); 43 // Delete the ConfigWatcher to join the notifier thread, ensuring that 44 // StartReachabilityNotifications() has an opportunity to run to completion. 45 config_watcher_.reset(); 46 47 // Now that StartReachabilityNotifications() has either run to completion or 48 // never run at all, unschedule reachability_ if it was previously scheduled. 49 if (reachability_.get() && run_loop_.get()) { 50 SCNetworkReachabilityUnscheduleFromRunLoop( 51 reachability_.get(), run_loop_.get(), kCFRunLoopCommonModes); 52 } 53} 54 55// static 56NetworkChangeNotifier::NetworkChangeCalculatorParams 57NetworkChangeNotifierMac::NetworkChangeCalculatorParamsMac() { 58 NetworkChangeCalculatorParams params; 59 // Delay values arrived at by simple experimentation and adjusted so as to 60 // produce a single signal when switching between network connections. 61 params.ip_address_offline_delay_ = base::TimeDelta::FromMilliseconds(500); 62 params.ip_address_online_delay_ = base::TimeDelta::FromMilliseconds(500); 63 params.connection_type_offline_delay_ = 64 base::TimeDelta::FromMilliseconds(1000); 65 params.connection_type_online_delay_ = base::TimeDelta::FromMilliseconds(500); 66 return params; 67} 68 69NetworkChangeNotifier::ConnectionType 70NetworkChangeNotifierMac::GetCurrentConnectionType() const { 71 // https://crbug.com/125097 72 base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait; 73 base::AutoLock lock(connection_type_lock_); 74 // Make sure the initial connection type is set before returning. 75 while (!connection_type_initialized_) { 76 initial_connection_type_cv_.Wait(); 77 } 78 return connection_type_; 79} 80 81void NetworkChangeNotifierMac::Forwarder::Init() { 82 net_config_watcher_->SetInitialConnectionType(); 83} 84 85// static 86NetworkChangeNotifier::ConnectionType 87NetworkChangeNotifierMac::CalculateConnectionType( 88 SCNetworkConnectionFlags flags) { 89 bool reachable = CalculateReachability(flags); 90 if (!reachable) 91 return CONNECTION_NONE; 92 93#if defined(OS_IOS) 94 if (!(flags & kSCNetworkReachabilityFlagsIsWWAN)) { 95 return CONNECTION_WIFI; 96 } 97 if (@available(iOS 12, *)) { 98 CTTelephonyNetworkInfo* info = 99 [[[CTTelephonyNetworkInfo alloc] init] autorelease]; 100 NSDictionary<NSString*, NSString*>* 101 service_current_radio_access_technology = 102 [info serviceCurrentRadioAccessTechnology]; 103 NSSet<NSString*>* technologies_2g = [NSSet 104 setWithObjects:CTRadioAccessTechnologyGPRS, CTRadioAccessTechnologyGPRS, 105 CTRadioAccessTechnologyCDMA1x, nil]; 106 NSSet<NSString*>* technologies_3g = 107 [NSSet setWithObjects:CTRadioAccessTechnologyWCDMA, 108 CTRadioAccessTechnologyHSDPA, 109 CTRadioAccessTechnologyHSUPA, 110 CTRadioAccessTechnologyCDMAEVDORev0, 111 CTRadioAccessTechnologyCDMAEVDORevA, 112 CTRadioAccessTechnologyCDMAEVDORevB, 113 CTRadioAccessTechnologyeHRPD, nil]; 114 NSSet<NSString*>* technologies_4g = 115 [NSSet setWithObjects:CTRadioAccessTechnologyLTE, nil]; 116 // TODO: Use constants from CoreTelephony once Cronet builds with XCode 12.1 117 NSSet<NSString*>* technologies_5g = 118 [NSSet setWithObjects:@"CTRadioAccessTechnologyNRNSA", 119 @"CTRadioAccessTechnologyNR", nil]; 120 int best_network = 0; 121 for (NSString* service in service_current_radio_access_technology) { 122 if (!service_current_radio_access_technology[service]) { 123 continue; 124 } 125 int current_network = 0; 126 127 NSString* network_type = service_current_radio_access_technology[service]; 128 129 if ([technologies_2g containsObject:network_type]) { 130 current_network = 2; 131 } else if ([technologies_3g containsObject:network_type]) { 132 current_network = 3; 133 } else if ([technologies_4g containsObject:network_type]) { 134 current_network = 4; 135 } else if ([technologies_5g containsObject:network_type]) { 136 current_network = 5; 137 } else { 138 // New technology? 139 NOTREACHED(); 140 return CONNECTION_UNKNOWN; 141 } 142 if (current_network > best_network) { 143 // iOS is supposed to use the best network available. 144 best_network = current_network; 145 } 146 } 147 switch (best_network) { 148 case 2: 149 return CONNECTION_2G; 150 case 3: 151 return CONNECTION_3G; 152 case 4: 153 return CONNECTION_4G; 154 case 5: 155 return CONNECTION_5G; 156 default: 157 // Default to CONNECTION_3G to not change existing behavior. 158 return CONNECTION_3G; 159 } 160 } else { 161 return CONNECTION_3G; 162 } 163 164#else 165 return ConnectionTypeFromInterfaces(); 166#endif 167} 168 169void NetworkChangeNotifierMac::Forwarder::StartReachabilityNotifications() { 170 net_config_watcher_->StartReachabilityNotifications(); 171} 172 173void NetworkChangeNotifierMac::Forwarder::SetDynamicStoreNotificationKeys( 174 SCDynamicStoreRef store) { 175 net_config_watcher_->SetDynamicStoreNotificationKeys(store); 176} 177 178void NetworkChangeNotifierMac::Forwarder::OnNetworkConfigChange( 179 CFArrayRef changed_keys) { 180 net_config_watcher_->OnNetworkConfigChange(changed_keys); 181} 182 183void NetworkChangeNotifierMac::SetInitialConnectionType() { 184 // Called on notifier thread. 185 186 // Try to reach 0.0.0.0. This is the approach taken by Firefox: 187 // 188 // http://mxr.mozilla.org/mozilla2.0/source/netwerk/system/mac/nsNetworkLinkService.mm 189 // 190 // From my (adamk) testing on Snow Leopard, 0.0.0.0 191 // seems to be reachable if any network connection is available. 192 struct sockaddr_in addr = {0}; 193 addr.sin_len = sizeof(addr); 194 addr.sin_family = AF_INET; 195 reachability_.reset(SCNetworkReachabilityCreateWithAddress( 196 kCFAllocatorDefault, reinterpret_cast<struct sockaddr*>(&addr))); 197 198 SCNetworkConnectionFlags flags; 199 ConnectionType connection_type = CONNECTION_UNKNOWN; 200 if (SCNetworkReachabilityGetFlags(reachability_, &flags)) { 201 connection_type = CalculateConnectionType(flags); 202 } else { 203 LOG(ERROR) << "Could not get initial network connection type," 204 << "assuming online."; 205 } 206 { 207 base::AutoLock lock(connection_type_lock_); 208 connection_type_ = connection_type; 209 connection_type_initialized_ = true; 210 initial_connection_type_cv_.Broadcast(); 211 } 212} 213 214void NetworkChangeNotifierMac::StartReachabilityNotifications() { 215 // Called on notifier thread. 216 run_loop_.reset(CFRunLoopGetCurrent()); 217 CFRetain(run_loop_.get()); 218 219 DCHECK(reachability_); 220 SCNetworkReachabilityContext reachability_context = { 221 0, // version 222 this, // user data 223 NULL, // retain 224 NULL, // release 225 NULL // description 226 }; 227 if (!SCNetworkReachabilitySetCallback( 228 reachability_, &NetworkChangeNotifierMac::ReachabilityCallback, 229 &reachability_context)) { 230 LOG(DFATAL) << "Could not set network reachability callback"; 231 reachability_.reset(); 232 } else if (!SCNetworkReachabilityScheduleWithRunLoop(reachability_, run_loop_, 233 kCFRunLoopCommonModes)) { 234 LOG(DFATAL) << "Could not schedule network reachability on run loop"; 235 reachability_.reset(); 236 } 237} 238 239void NetworkChangeNotifierMac::SetDynamicStoreNotificationKeys( 240 SCDynamicStoreRef store) { 241#if defined(OS_IOS) 242 // SCDynamicStore API does not exist on iOS. 243 NOTREACHED(); 244#else 245 base::ScopedCFTypeRef<CFMutableArrayRef> notification_keys( 246 CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks)); 247 base::ScopedCFTypeRef<CFStringRef> key( 248 SCDynamicStoreKeyCreateNetworkGlobalEntity( 249 NULL, kSCDynamicStoreDomainState, kSCEntNetInterface)); 250 CFArrayAppendValue(notification_keys.get(), key.get()); 251 key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity( 252 NULL, kSCDynamicStoreDomainState, kSCEntNetIPv4)); 253 CFArrayAppendValue(notification_keys.get(), key.get()); 254 key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity( 255 NULL, kSCDynamicStoreDomainState, kSCEntNetIPv6)); 256 CFArrayAppendValue(notification_keys.get(), key.get()); 257 258 // Set the notification keys. This starts us receiving notifications. 259 bool ret = 260 SCDynamicStoreSetNotificationKeys(store, notification_keys.get(), NULL); 261 // TODO(willchan): Figure out a proper way to handle this rather than crash. 262 CHECK(ret); 263#endif // defined(OS_IOS) 264} 265 266void NetworkChangeNotifierMac::OnNetworkConfigChange(CFArrayRef changed_keys) { 267#if defined(OS_IOS) 268 // SCDynamicStore API does not exist on iOS. 269 NOTREACHED(); 270#else 271 DCHECK_EQ(run_loop_.get(), CFRunLoopGetCurrent()); 272 273 for (CFIndex i = 0; i < CFArrayGetCount(changed_keys); ++i) { 274 CFStringRef key = 275 static_cast<CFStringRef>(CFArrayGetValueAtIndex(changed_keys, i)); 276 if (CFStringHasSuffix(key, kSCEntNetIPv4) || 277 CFStringHasSuffix(key, kSCEntNetIPv6)) { 278 NotifyObserversOfIPAddressChange(); 279 return; 280 } 281 if (CFStringHasSuffix(key, kSCEntNetInterface)) { 282 // TODO(willchan): Does not appear to be working. Look into this. 283 // Perhaps this isn't needed anyway. 284 } else { 285 NOTREACHED(); 286 } 287 } 288#endif // defined(OS_IOS) 289} 290 291// static 292void NetworkChangeNotifierMac::ReachabilityCallback( 293 SCNetworkReachabilityRef target, 294 SCNetworkConnectionFlags flags, 295 void* notifier) { 296 NetworkChangeNotifierMac* notifier_mac = 297 static_cast<NetworkChangeNotifierMac*>(notifier); 298 299 DCHECK_EQ(notifier_mac->run_loop_.get(), CFRunLoopGetCurrent()); 300 301 ConnectionType new_type = CalculateConnectionType(flags); 302 ConnectionType old_type; 303 { 304 base::AutoLock lock(notifier_mac->connection_type_lock_); 305 old_type = notifier_mac->connection_type_; 306 notifier_mac->connection_type_ = new_type; 307 } 308 if (old_type != new_type) { 309 NotifyObserversOfConnectionTypeChange(); 310 double max_bandwidth_mbps = 311 NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype( 312 new_type == CONNECTION_NONE ? SUBTYPE_NONE : SUBTYPE_UNKNOWN); 313 NotifyObserversOfMaxBandwidthChange(max_bandwidth_mbps, new_type); 314 } 315 316#if defined(OS_IOS) 317 // On iOS, the SCDynamicStore API does not exist, and we use the reachability 318 // API to detect IP address changes instead. 319 NotifyObserversOfIPAddressChange(); 320#endif // defined(OS_IOS) 321} 322 323} // namespace net 324