• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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