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