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