xref: /aosp_15_r20/external/cronet/net/base/network_change_notifier_apple.mm (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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