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/basictypes.h"
11#include "base/threading/thread.h"
12#include "net/dns/dns_config_service.h"
13
14namespace net {
15
16static bool CalculateReachability(SCNetworkConnectionFlags flags) {
17  bool reachable = flags & kSCNetworkFlagsReachable;
18  bool connection_required = flags & kSCNetworkFlagsConnectionRequired;
19  return reachable && !connection_required;
20}
21
22NetworkChangeNotifier::ConnectionType CalculateConnectionType(
23    SCNetworkConnectionFlags flags) {
24  bool reachable = CalculateReachability(flags);
25  if (reachable) {
26#if defined(OS_IOS)
27    return (flags & kSCNetworkReachabilityFlagsIsWWAN) ?
28        NetworkChangeNotifier::CONNECTION_3G :
29        NetworkChangeNotifier::CONNECTION_WIFI;
30#else
31    // TODO(droger): Get something more detailed than CONNECTION_UNKNOWN.
32    // http://crbug.com/112937
33    return NetworkChangeNotifier::CONNECTION_UNKNOWN;
34#endif  // defined(OS_IOS)
35  } else {
36    return NetworkChangeNotifier::CONNECTION_NONE;
37  }
38}
39
40// Thread on which we can run DnsConfigService, which requires a TYPE_IO
41// message loop.
42class NetworkChangeNotifierMac::DnsConfigServiceThread : public base::Thread {
43 public:
44  DnsConfigServiceThread() : base::Thread("DnsConfigService") {}
45
46  virtual ~DnsConfigServiceThread() {
47    Stop();
48  }
49
50  virtual void Init() OVERRIDE {
51    service_ = DnsConfigService::CreateSystemService();
52    service_->WatchConfig(base::Bind(&NetworkChangeNotifier::SetDnsConfig));
53  }
54
55  virtual void CleanUp() OVERRIDE {
56    service_.reset();
57  }
58
59 private:
60  scoped_ptr<DnsConfigService> service_;
61
62  DISALLOW_COPY_AND_ASSIGN(DnsConfigServiceThread);
63};
64
65NetworkChangeNotifierMac::NetworkChangeNotifierMac()
66    : NetworkChangeNotifier(NetworkChangeCalculatorParamsMac()),
67      connection_type_(CONNECTION_UNKNOWN),
68      connection_type_initialized_(false),
69      initial_connection_type_cv_(&connection_type_lock_),
70      forwarder_(this),
71      dns_config_service_thread_(new DnsConfigServiceThread()) {
72  // Must be initialized after the rest of this object, as it may call back into
73  // SetInitialConnectionType().
74  config_watcher_.reset(new NetworkConfigWatcherMac(&forwarder_));
75  dns_config_service_thread_->StartWithOptions(
76      base::Thread::Options(base::MessageLoop::TYPE_IO, 0));
77}
78
79NetworkChangeNotifierMac::~NetworkChangeNotifierMac() {
80  // Delete the ConfigWatcher to join the notifier thread, ensuring that
81  // StartReachabilityNotifications() has an opportunity to run to completion.
82  config_watcher_.reset();
83
84  // Now that StartReachabilityNotifications() has either run to completion or
85  // never run at all, unschedule reachability_ if it was previously scheduled.
86  if (reachability_.get() && run_loop_.get()) {
87    SCNetworkReachabilityUnscheduleFromRunLoop(reachability_.get(),
88                                               run_loop_.get(),
89                                               kCFRunLoopCommonModes);
90  }
91}
92
93// static
94NetworkChangeNotifier::NetworkChangeCalculatorParams
95NetworkChangeNotifierMac::NetworkChangeCalculatorParamsMac() {
96  NetworkChangeCalculatorParams params;
97  // Delay values arrived at by simple experimentation and adjusted so as to
98  // produce a single signal when switching between network connections.
99  params.ip_address_offline_delay_ = base::TimeDelta::FromMilliseconds(500);
100  params.ip_address_online_delay_ = base::TimeDelta::FromMilliseconds(500);
101  params.connection_type_offline_delay_ =
102      base::TimeDelta::FromMilliseconds(1000);
103  params.connection_type_online_delay_ = base::TimeDelta::FromMilliseconds(500);
104  return params;
105}
106
107NetworkChangeNotifier::ConnectionType
108NetworkChangeNotifierMac::GetCurrentConnectionType() const {
109  base::AutoLock lock(connection_type_lock_);
110  // Make sure the initial connection type is set before returning.
111  while (!connection_type_initialized_) {
112    initial_connection_type_cv_.Wait();
113  }
114  return connection_type_;
115}
116
117void NetworkChangeNotifierMac::Forwarder::Init()  {
118  net_config_watcher_->SetInitialConnectionType();
119}
120
121void NetworkChangeNotifierMac::Forwarder::StartReachabilityNotifications() {
122  net_config_watcher_->StartReachabilityNotifications();
123}
124
125void NetworkChangeNotifierMac::Forwarder::SetDynamicStoreNotificationKeys(
126    SCDynamicStoreRef store)  {
127  net_config_watcher_->SetDynamicStoreNotificationKeys(store);
128}
129
130void NetworkChangeNotifierMac::Forwarder::OnNetworkConfigChange(
131    CFArrayRef changed_keys)  {
132  net_config_watcher_->OnNetworkConfigChange(changed_keys);
133}
134
135void NetworkChangeNotifierMac::SetInitialConnectionType() {
136  // Called on notifier thread.
137
138  // Try to reach 0.0.0.0. This is the approach taken by Firefox:
139  //
140  // http://mxr.mozilla.org/mozilla2.0/source/netwerk/system/mac/nsNetworkLinkService.mm
141  //
142  // From my (adamk) testing on Snow Leopard, 0.0.0.0
143  // seems to be reachable if any network connection is available.
144  struct sockaddr_in addr = {0};
145  addr.sin_len = sizeof(addr);
146  addr.sin_family = AF_INET;
147  reachability_.reset(SCNetworkReachabilityCreateWithAddress(
148      kCFAllocatorDefault, reinterpret_cast<struct sockaddr*>(&addr)));
149
150  SCNetworkConnectionFlags flags;
151  ConnectionType connection_type = CONNECTION_UNKNOWN;
152  if (SCNetworkReachabilityGetFlags(reachability_, &flags)) {
153    connection_type = CalculateConnectionType(flags);
154  } else {
155    LOG(ERROR) << "Could not get initial network connection type,"
156               << "assuming online.";
157  }
158  {
159    base::AutoLock lock(connection_type_lock_);
160    connection_type_ = connection_type;
161    connection_type_initialized_ = true;
162    initial_connection_type_cv_.Signal();
163  }
164}
165
166void NetworkChangeNotifierMac::StartReachabilityNotifications() {
167  // Called on notifier thread.
168  run_loop_.reset(CFRunLoopGetCurrent());
169  CFRetain(run_loop_.get());
170
171  DCHECK(reachability_);
172  SCNetworkReachabilityContext reachability_context = {
173    0,     // version
174    this,  // user data
175    NULL,  // retain
176    NULL,  // release
177    NULL   // description
178  };
179  if (!SCNetworkReachabilitySetCallback(
180          reachability_,
181          &NetworkChangeNotifierMac::ReachabilityCallback,
182          &reachability_context)) {
183    LOG(DFATAL) << "Could not set network reachability callback";
184    reachability_.reset();
185  } else if (!SCNetworkReachabilityScheduleWithRunLoop(reachability_,
186                                                       run_loop_,
187                                                       kCFRunLoopCommonModes)) {
188    LOG(DFATAL) << "Could not schedule network reachability on run loop";
189    reachability_.reset();
190  }
191}
192
193void NetworkChangeNotifierMac::SetDynamicStoreNotificationKeys(
194    SCDynamicStoreRef store) {
195#if defined(OS_IOS)
196  // SCDynamicStore API does not exist on iOS.
197  NOTREACHED();
198#else
199  base::ScopedCFTypeRef<CFMutableArrayRef> notification_keys(
200      CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks));
201  base::ScopedCFTypeRef<CFStringRef> key(
202      SCDynamicStoreKeyCreateNetworkGlobalEntity(
203          NULL, kSCDynamicStoreDomainState, kSCEntNetInterface));
204  CFArrayAppendValue(notification_keys.get(), key.get());
205  key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity(
206      NULL, kSCDynamicStoreDomainState, kSCEntNetIPv4));
207  CFArrayAppendValue(notification_keys.get(), key.get());
208  key.reset(SCDynamicStoreKeyCreateNetworkGlobalEntity(
209      NULL, kSCDynamicStoreDomainState, kSCEntNetIPv6));
210  CFArrayAppendValue(notification_keys.get(), key.get());
211
212  // Set the notification keys.  This starts us receiving notifications.
213  bool ret = SCDynamicStoreSetNotificationKeys(
214      store, notification_keys.get(), NULL);
215  // TODO(willchan): Figure out a proper way to handle this rather than crash.
216  CHECK(ret);
217#endif  // defined(OS_IOS)
218}
219
220void NetworkChangeNotifierMac::OnNetworkConfigChange(CFArrayRef changed_keys) {
221#if defined(OS_IOS)
222  // SCDynamicStore API does not exist on iOS.
223  NOTREACHED();
224#else
225  DCHECK_EQ(run_loop_.get(), CFRunLoopGetCurrent());
226
227  for (CFIndex i = 0; i < CFArrayGetCount(changed_keys); ++i) {
228    CFStringRef key = static_cast<CFStringRef>(
229        CFArrayGetValueAtIndex(changed_keys, i));
230    if (CFStringHasSuffix(key, kSCEntNetIPv4) ||
231        CFStringHasSuffix(key, kSCEntNetIPv6)) {
232      NotifyObserversOfIPAddressChange();
233      return;
234    }
235    if (CFStringHasSuffix(key, kSCEntNetInterface)) {
236      // TODO(willchan): Does not appear to be working.  Look into this.
237      // Perhaps this isn't needed anyway.
238    } else {
239      NOTREACHED();
240    }
241  }
242#endif  // defined(OS_IOS)
243}
244
245// static
246void NetworkChangeNotifierMac::ReachabilityCallback(
247    SCNetworkReachabilityRef target,
248    SCNetworkConnectionFlags flags,
249    void* notifier) {
250  NetworkChangeNotifierMac* notifier_mac =
251      static_cast<NetworkChangeNotifierMac*>(notifier);
252
253  DCHECK_EQ(notifier_mac->run_loop_.get(), CFRunLoopGetCurrent());
254
255  ConnectionType new_type = CalculateConnectionType(flags);
256  ConnectionType old_type;
257  {
258    base::AutoLock lock(notifier_mac->connection_type_lock_);
259    old_type = notifier_mac->connection_type_;
260    notifier_mac->connection_type_ = new_type;
261  }
262  if (old_type != new_type)
263    NotifyObserversOfConnectionTypeChange();
264
265#if defined(OS_IOS)
266  // On iOS, the SCDynamicStore API does not exist, and we use the reachability
267  // API to detect IP address changes instead.
268  NotifyObserversOfIPAddressChange();
269#endif  // defined(OS_IOS)
270}
271
272}  // namespace net
273