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_apple.h" 6 7#include <netinet/in.h> 8#include <resolv.h> 9 10#include "base/apple/bridging.h" 11#include "base/apple/foundation_util.h" 12#include "base/feature_list.h" 13#include "base/functional/bind.h" 14#include "base/functional/callback.h" 15#include "base/logging.h" 16#include "base/memory/scoped_policy.h" 17#include "base/metrics/histogram_macros.h" 18#include "base/strings/string_number_conversions.h" 19#include "base/strings/sys_string_conversions.h" 20#include "base/task/sequenced_task_runner.h" 21#include "base/task/task_traits.h" 22#include "base/threading/thread_restrictions.h" 23#include "build/build_config.h" 24#include "net/base/features.h" 25#include "net/base/network_interfaces_getifaddrs.h" 26#include "net/dns/dns_config_service.h" 27 28#if BUILDFLAG(IS_IOS) 29#import <CoreTelephony/CTTelephonyNetworkInfo.h> 30#endif 31 32namespace { 33// The maximum number of seconds to wait for the connection type to be 34// determined. 35const double kMaxWaitForConnectionTypeInSeconds = 2.0; 36 37#if BUILDFLAG(IS_MAC) 38std::string GetIPv6PrimaryInterfaceName(SCDynamicStoreRef store) { 39 base::apple::ScopedCFTypeRef<CFStringRef> ipv6netkey( 40 SCDynamicStoreKeyCreateNetworkGlobalEntity( 41 nullptr, kSCDynamicStoreDomainState, kSCEntNetIPv6)); 42 base::apple::ScopedCFTypeRef<CFPropertyListRef> ipv6netdict_value( 43 SCDynamicStoreCopyValue(store, ipv6netkey.get())); 44 CFDictionaryRef ipv6netdict = 45 base::apple::CFCast<CFDictionaryRef>(ipv6netdict_value.get()); 46 if (!ipv6netdict) { 47 return ""; 48 } 49 CFStringRef primary_if_name_ref = 50 base::apple::GetValueFromDictionary<CFStringRef>( 51 ipv6netdict, kSCDynamicStorePropNetPrimaryInterface); 52 if (!primary_if_name_ref) { 53 return ""; 54 } 55 return base::SysCFStringRefToUTF8(primary_if_name_ref); 56} 57 58std::optional<net::NetworkInterfaceList> 59GetNetworkInterfaceListForNetworkChangeCheck( 60 base::RepeatingCallback<bool(net::NetworkInterfaceList*, int)> 61 get_network_list_callback, 62 base::RepeatingCallback<std::string(SCDynamicStoreRef)> 63 get_ipv6_primary_interface_name_callback, 64 SCDynamicStoreRef store) { 65 net::NetworkInterfaceList interfaces; 66 if (!get_network_list_callback.Run( 67 &interfaces, net::EXCLUDE_HOST_SCOPE_VIRTUAL_INTERFACES)) { 68 return std::nullopt; 69 } 70 const std::string ipv6_primary_interface_name = 71 get_ipv6_primary_interface_name_callback.Run(store); 72 std::erase_if(interfaces, [&ipv6_primary_interface_name]( 73 const net::NetworkInterface& interface) { 74 return interface.address.IsIPv6() && 75 !interface.address.IsPubliclyRoutable() && 76 (interface.name != ipv6_primary_interface_name); 77 }); 78 return interfaces; 79} 80#endif // BUILDFLAG(IS_MAC) 81 82} // namespace 83 84namespace net { 85 86static bool CalculateReachability(SCNetworkConnectionFlags flags) { 87 bool reachable = flags & kSCNetworkFlagsReachable; 88 bool connection_required = flags & kSCNetworkFlagsConnectionRequired; 89 return reachable && !connection_required; 90} 91 92NetworkChangeNotifierApple::NetworkChangeNotifierApple() 93 : NetworkChangeNotifier(NetworkChangeCalculatorParamsMac()), 94 initial_connection_type_cv_(&connection_type_lock_), 95 forwarder_(this) 96#if BUILDFLAG(IS_MAC) 97 , 98 reduce_ip_address_change_notification_(base::FeatureList::IsEnabled( 99 features::kReduceIPAddressChangeNotification)), 100 get_network_list_callback_(base::BindRepeating(&GetNetworkList)), 101 get_ipv6_primary_interface_name_callback_( 102 base::BindRepeating(&GetIPv6PrimaryInterfaceName)) 103#endif // BUILDFLAG(IS_MAC) 104{ 105 // Must be initialized after the rest of this object, as it may call back into 106 // SetInitialConnectionType(). 107 config_watcher_ = std::make_unique<NetworkConfigWatcherApple>(&forwarder_); 108} 109 110NetworkChangeNotifierApple::~NetworkChangeNotifierApple() { 111 ClearGlobalPointer(); 112 // Delete the ConfigWatcher to join the notifier thread, ensuring that 113 // StartReachabilityNotifications() has an opportunity to run to completion. 114 config_watcher_.reset(); 115 116 // Now that StartReachabilityNotifications() has either run to completion or 117 // never run at all, unschedule reachability_ if it was previously scheduled. 118 if (reachability_.get() && run_loop_.get()) { 119 SCNetworkReachabilityUnscheduleFromRunLoop( 120 reachability_.get(), run_loop_.get(), kCFRunLoopCommonModes); 121 } 122} 123 124// static 125NetworkChangeNotifier::NetworkChangeCalculatorParams 126NetworkChangeNotifierApple::NetworkChangeCalculatorParamsMac() { 127 NetworkChangeCalculatorParams params; 128 // Delay values arrived at by simple experimentation and adjusted so as to 129 // produce a single signal when switching between network connections. 130 params.ip_address_offline_delay_ = base::Milliseconds(500); 131 params.ip_address_online_delay_ = base::Milliseconds(500); 132 params.connection_type_offline_delay_ = base::Milliseconds(1000); 133 params.connection_type_online_delay_ = base::Milliseconds(500); 134 return params; 135} 136 137NetworkChangeNotifier::ConnectionType 138NetworkChangeNotifierApple::GetCurrentConnectionType() const { 139 // https://crbug.com/125097 140 base::ScopedAllowBaseSyncPrimitivesOutsideBlockingScope allow_wait; 141 base::AutoLock lock(connection_type_lock_); 142 143 if (connection_type_initialized_) 144 return connection_type_; 145 146 // Wait up to a limited amount of time for the connection type to be 147 // determined, to avoid blocking the main thread indefinitely. Since 148 // ConditionVariables are susceptible to spurious wake-ups, each call to 149 // TimedWait can spuriously return even though the connection type hasn't been 150 // initialized and the timeout hasn't been reached; so TimedWait must be 151 // called repeatedly until either the timeout is reached or the connection 152 // type has been determined. 153 base::TimeDelta remaining_time = 154 base::Seconds(kMaxWaitForConnectionTypeInSeconds); 155 base::TimeTicks end_time = base::TimeTicks::Now() + remaining_time; 156 while (remaining_time.is_positive()) { 157 initial_connection_type_cv_.TimedWait(remaining_time); 158 if (connection_type_initialized_) 159 return connection_type_; 160 161 remaining_time = end_time - base::TimeTicks::Now(); 162 } 163 164 return CONNECTION_UNKNOWN; 165} 166 167void NetworkChangeNotifierApple::Forwarder::Init() { 168 net_config_watcher_->SetInitialConnectionType(); 169} 170 171// static 172NetworkChangeNotifier::ConnectionType 173NetworkChangeNotifierApple::CalculateConnectionType( 174 SCNetworkConnectionFlags flags) { 175 bool reachable = CalculateReachability(flags); 176 if (!reachable) 177 return CONNECTION_NONE; 178 179#if BUILDFLAG(IS_IOS) 180 if (!(flags & kSCNetworkReachabilityFlagsIsWWAN)) { 181 return CONNECTION_WIFI; 182 } 183 if (@available(iOS 12, *)) { 184 CTTelephonyNetworkInfo* info = [[CTTelephonyNetworkInfo alloc] init]; 185 NSDictionary<NSString*, NSString*>* 186 service_current_radio_access_technology = 187 info.serviceCurrentRadioAccessTechnology; 188 NSSet<NSString*>* technologies_2g = [NSSet 189 setWithObjects:CTRadioAccessTechnologyGPRS, CTRadioAccessTechnologyEdge, 190 CTRadioAccessTechnologyCDMA1x, nil]; 191 NSSet<NSString*>* technologies_3g = 192 [NSSet setWithObjects:CTRadioAccessTechnologyWCDMA, 193 CTRadioAccessTechnologyHSDPA, 194 CTRadioAccessTechnologyHSUPA, 195 CTRadioAccessTechnologyCDMAEVDORev0, 196 CTRadioAccessTechnologyCDMAEVDORevA, 197 CTRadioAccessTechnologyCDMAEVDORevB, 198 CTRadioAccessTechnologyeHRPD, nil]; 199 NSSet<NSString*>* technologies_4g = 200 [NSSet setWithObjects:CTRadioAccessTechnologyLTE, nil]; 201 // TODO: Use constants from CoreTelephony once Cronet builds with XCode 12.1 202 NSSet<NSString*>* technologies_5g = 203 [NSSet setWithObjects:@"CTRadioAccessTechnologyNRNSA", 204 @"CTRadioAccessTechnologyNR", nil]; 205 int best_network = 0; 206 for (NSString* service in service_current_radio_access_technology) { 207 if (!service_current_radio_access_technology[service]) { 208 continue; 209 } 210 int current_network = 0; 211 212 NSString* network_type = service_current_radio_access_technology[service]; 213 214 if ([technologies_2g containsObject:network_type]) { 215 current_network = 2; 216 } else if ([technologies_3g containsObject:network_type]) { 217 current_network = 3; 218 } else if ([technologies_4g containsObject:network_type]) { 219 current_network = 4; 220 } else if ([technologies_5g containsObject:network_type]) { 221 current_network = 5; 222 } else { 223 // New technology? 224 NOTREACHED() << "Unknown network technology: " << network_type; 225 return CONNECTION_UNKNOWN; 226 } 227 if (current_network > best_network) { 228 // iOS is supposed to use the best network available. 229 best_network = current_network; 230 } 231 } 232 switch (best_network) { 233 case 2: 234 return CONNECTION_2G; 235 case 3: 236 return CONNECTION_3G; 237 case 4: 238 return CONNECTION_4G; 239 case 5: 240 return CONNECTION_5G; 241 default: 242 // Default to CONNECTION_3G to not change existing behavior. 243 return CONNECTION_3G; 244 } 245 } else { 246 return CONNECTION_3G; 247 } 248 249#else 250 return ConnectionTypeFromInterfaces(); 251#endif 252} 253 254void NetworkChangeNotifierApple::Forwarder::StartReachabilityNotifications() { 255 net_config_watcher_->StartReachabilityNotifications(); 256} 257 258void NetworkChangeNotifierApple::Forwarder::SetDynamicStoreNotificationKeys( 259 base::apple::ScopedCFTypeRef<SCDynamicStoreRef> store) { 260 net_config_watcher_->SetDynamicStoreNotificationKeys(std::move(store)); 261} 262 263void NetworkChangeNotifierApple::Forwarder::OnNetworkConfigChange( 264 CFArrayRef changed_keys) { 265 net_config_watcher_->OnNetworkConfigChange(changed_keys); 266} 267 268void NetworkChangeNotifierApple::Forwarder::CleanUpOnNotifierThread() { 269 net_config_watcher_->CleanUpOnNotifierThread(); 270} 271 272void NetworkChangeNotifierApple::SetInitialConnectionType() { 273 // Called on notifier thread. 274 275 // Try to reach 0.0.0.0. This is the approach taken by Firefox: 276 // 277 // http://mxr.mozilla.org/mozilla2.0/source/netwerk/system/mac/nsNetworkLinkService.mm 278 // 279 // From my (adamk) testing on Snow Leopard, 0.0.0.0 280 // seems to be reachable if any network connection is available. 281 struct sockaddr_in addr = {0}; 282 addr.sin_len = sizeof(addr); 283 addr.sin_family = AF_INET; 284 reachability_.reset(SCNetworkReachabilityCreateWithAddress( 285 kCFAllocatorDefault, reinterpret_cast<struct sockaddr*>(&addr))); 286 287 SCNetworkConnectionFlags flags; 288 ConnectionType connection_type = CONNECTION_UNKNOWN; 289 if (SCNetworkReachabilityGetFlags(reachability_.get(), &flags)) { 290 connection_type = CalculateConnectionType(flags); 291 } else { 292 LOG(ERROR) << "Could not get initial network connection type," 293 << "assuming online."; 294 } 295 { 296 base::AutoLock lock(connection_type_lock_); 297 connection_type_ = connection_type; 298 connection_type_initialized_ = true; 299 initial_connection_type_cv_.Broadcast(); 300 } 301} 302 303void NetworkChangeNotifierApple::StartReachabilityNotifications() { 304 // Called on notifier thread. 305 run_loop_.reset(CFRunLoopGetCurrent(), base::scoped_policy::RETAIN); 306 307 DCHECK(reachability_); 308 SCNetworkReachabilityContext reachability_context = { 309 0, // version 310 this, // user data 311 nullptr, // retain 312 nullptr, // release 313 nullptr // description 314 }; 315 if (!SCNetworkReachabilitySetCallback( 316 reachability_.get(), &NetworkChangeNotifierApple::ReachabilityCallback, 317 &reachability_context)) { 318 LOG(DFATAL) << "Could not set network reachability callback"; 319 reachability_.reset(); 320 } else if (!SCNetworkReachabilityScheduleWithRunLoop( 321 reachability_.get(), run_loop_.get(), kCFRunLoopCommonModes)) { 322 LOG(DFATAL) << "Could not schedule network reachability on run loop"; 323 reachability_.reset(); 324 } 325} 326 327void NetworkChangeNotifierApple::SetDynamicStoreNotificationKeys( 328 base::apple::ScopedCFTypeRef<SCDynamicStoreRef> store) { 329#if BUILDFLAG(IS_IOS) 330 // SCDynamicStore API does not exist on iOS. 331 NOTREACHED(); 332#elif BUILDFLAG(IS_MAC) 333 NSArray* notification_keys = @[ 334 base::apple::CFToNSOwnershipCast(SCDynamicStoreKeyCreateNetworkGlobalEntity( 335 nullptr, kSCDynamicStoreDomainState, kSCEntNetInterface)), 336 base::apple::CFToNSOwnershipCast(SCDynamicStoreKeyCreateNetworkGlobalEntity( 337 nullptr, kSCDynamicStoreDomainState, kSCEntNetIPv4)), 338 base::apple::CFToNSOwnershipCast(SCDynamicStoreKeyCreateNetworkGlobalEntity( 339 nullptr, kSCDynamicStoreDomainState, kSCEntNetIPv6)), 340 ]; 341 342 // Set the notification keys. This starts us receiving notifications. 343 bool ret = SCDynamicStoreSetNotificationKeys( 344 store.get(), base::apple::NSToCFPtrCast(notification_keys), 345 /*patterns=*/nullptr); 346 // TODO(willchan): Figure out a proper way to handle this rather than crash. 347 CHECK(ret); 348 349 if (reduce_ip_address_change_notification_) { 350 store_ = std::move(store); 351 interfaces_for_network_change_check_ = 352 GetNetworkInterfaceListForNetworkChangeCheck( 353 get_network_list_callback_, 354 get_ipv6_primary_interface_name_callback_, store_.get()); 355 } 356 if (initialized_callback_for_test_) { 357 std::move(initialized_callback_for_test_).Run(); 358 } 359#endif // BUILDFLAG(IS_IOS) / BUILDFLAG(IS_MAC) 360} 361 362void NetworkChangeNotifierApple::OnNetworkConfigChange(CFArrayRef changed_keys) { 363#if BUILDFLAG(IS_IOS) 364 // SCDynamicStore API does not exist on iOS. 365 NOTREACHED(); 366#elif BUILDFLAG(IS_MAC) 367 DCHECK_EQ(run_loop_.get(), CFRunLoopGetCurrent()); 368 369 bool maybe_notify = false; 370 for (CFIndex i = 0; i < CFArrayGetCount(changed_keys); ++i) { 371 CFStringRef key = 372 static_cast<CFStringRef>(CFArrayGetValueAtIndex(changed_keys, i)); 373 if (CFStringHasSuffix(key, kSCEntNetIPv4) || 374 CFStringHasSuffix(key, kSCEntNetIPv6)) { 375 maybe_notify = true; 376 break; 377 } 378 if (CFStringHasSuffix(key, kSCEntNetInterface)) { 379 // TODO(willchan): Does not appear to be working. Look into this. 380 // Perhaps this isn't needed anyway. 381 } else { 382 NOTREACHED(); 383 } 384 } 385 if (!maybe_notify) { 386 return; 387 } 388 if (!reduce_ip_address_change_notification_) { 389 NotifyObserversOfIPAddressChange(); 390 return; 391 } 392 393 std::optional<NetworkInterfaceList> interfaces = 394 GetNetworkInterfaceListForNetworkChangeCheck( 395 get_network_list_callback_, get_ipv6_primary_interface_name_callback_, 396 store_.get()); 397 if (interfaces_for_network_change_check_ && interfaces && 398 interfaces_for_network_change_check_.value() == interfaces.value()) { 399 return; 400 } 401 interfaces_for_network_change_check_ = std::move(interfaces); 402 NotifyObserversOfIPAddressChange(); 403#endif // BUILDFLAG(IS_IOS) 404} 405 406void NetworkChangeNotifierApple::CleanUpOnNotifierThread() { 407#if BUILDFLAG(IS_MAC) 408 store_.reset(); 409#endif // BUILDFLAG(IS_MAC) 410} 411 412// static 413void NetworkChangeNotifierApple::ReachabilityCallback( 414 SCNetworkReachabilityRef target, 415 SCNetworkConnectionFlags flags, 416 void* notifier) { 417 NetworkChangeNotifierApple* notifier_apple = 418 static_cast<NetworkChangeNotifierApple*>(notifier); 419 420 DCHECK_EQ(notifier_apple->run_loop_.get(), CFRunLoopGetCurrent()); 421 422 ConnectionType new_type = CalculateConnectionType(flags); 423 ConnectionType old_type; 424 { 425 base::AutoLock lock(notifier_apple->connection_type_lock_); 426 old_type = notifier_apple->connection_type_; 427 notifier_apple->connection_type_ = new_type; 428 } 429 if (old_type != new_type) { 430 NotifyObserversOfConnectionTypeChange(); 431 double max_bandwidth_mbps = 432 NetworkChangeNotifier::GetMaxBandwidthMbpsForConnectionSubtype( 433 new_type == CONNECTION_NONE ? SUBTYPE_NONE : SUBTYPE_UNKNOWN); 434 NotifyObserversOfMaxBandwidthChange(max_bandwidth_mbps, new_type); 435 } 436 437#if BUILDFLAG(IS_IOS) 438 // On iOS, the SCDynamicStore API does not exist, and we use the reachability 439 // API to detect IP address changes instead. 440 NotifyObserversOfIPAddressChange(); 441#endif // BUILDFLAG(IS_IOS) 442} 443 444#if BUILDFLAG(IS_MAC) 445void NetworkChangeNotifierApple::SetCallbacksForTest( 446 base::OnceClosure initialized_callback, 447 base::RepeatingCallback<bool(NetworkInterfaceList*, int)> 448 get_network_list_callback, 449 base::RepeatingCallback<std::string(SCDynamicStoreRef)> 450 get_ipv6_primary_interface_name_callback) { 451 initialized_callback_for_test_ = std::move(initialized_callback); 452 get_network_list_callback_ = std::move(get_network_list_callback); 453 get_ipv6_primary_interface_name_callback_ = 454 std::move(get_ipv6_primary_interface_name_callback); 455} 456#endif // BUILDFLAG(IS_MAC) 457 458} // namespace net 459