network_change_notifier_mac.mm 12 KB

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