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_win.h"
6
7#include <iphlpapi.h>
8#include <winsock2.h>
9
10#include "base/bind.h"
11#include "base/logging.h"
12#include "base/metrics/histogram.h"
13#include "base/threading/thread.h"
14#include "base/time/time.h"
15#include "net/base/winsock_init.h"
16#include "net/dns/dns_config_service.h"
17
18#pragma comment(lib, "iphlpapi.lib")
19
20namespace net {
21
22namespace {
23
24// Time between NotifyAddrChange retries, on failure.
25const int kWatchForAddressChangeRetryIntervalMs = 500;
26
27}  // namespace
28
29// Thread on which we can run DnsConfigService, which requires AssertIOAllowed
30// to open registry keys and to handle FilePathWatcher updates.
31class NetworkChangeNotifierWin::DnsConfigServiceThread : public base::Thread {
32 public:
33  DnsConfigServiceThread() : base::Thread("DnsConfigService") {}
34
35  virtual ~DnsConfigServiceThread() {
36    Stop();
37  }
38
39  virtual void Init() OVERRIDE {
40    service_ = DnsConfigService::CreateSystemService();
41    service_->WatchConfig(base::Bind(&NetworkChangeNotifier::SetDnsConfig));
42  }
43
44  virtual void CleanUp() OVERRIDE {
45    service_.reset();
46  }
47
48 private:
49  scoped_ptr<DnsConfigService> service_;
50
51  DISALLOW_COPY_AND_ASSIGN(DnsConfigServiceThread);
52};
53
54NetworkChangeNotifierWin::NetworkChangeNotifierWin()
55    : NetworkChangeNotifier(NetworkChangeCalculatorParamsWin()),
56      is_watching_(false),
57      sequential_failures_(0),
58      weak_factory_(this),
59      dns_config_service_thread_(new DnsConfigServiceThread()),
60      last_computed_connection_type_(RecomputeCurrentConnectionType()),
61      last_announced_offline_(
62          last_computed_connection_type_ == CONNECTION_NONE) {
63  memset(&addr_overlapped_, 0, sizeof addr_overlapped_);
64  addr_overlapped_.hEvent = WSACreateEvent();
65}
66
67NetworkChangeNotifierWin::~NetworkChangeNotifierWin() {
68  if (is_watching_) {
69    CancelIPChangeNotify(&addr_overlapped_);
70    addr_watcher_.StopWatching();
71  }
72  WSACloseEvent(addr_overlapped_.hEvent);
73}
74
75// static
76NetworkChangeNotifier::NetworkChangeCalculatorParams
77NetworkChangeNotifierWin::NetworkChangeCalculatorParamsWin() {
78  NetworkChangeCalculatorParams params;
79  // Delay values arrived at by simple experimentation and adjusted so as to
80  // produce a single signal when switching between network connections.
81  params.ip_address_offline_delay_ = base::TimeDelta::FromMilliseconds(1500);
82  params.ip_address_online_delay_ = base::TimeDelta::FromMilliseconds(1500);
83  params.connection_type_offline_delay_ =
84      base::TimeDelta::FromMilliseconds(1500);
85  params.connection_type_online_delay_ = base::TimeDelta::FromMilliseconds(500);
86  return params;
87}
88
89// This implementation does not return the actual connection type but merely
90// determines if the user is "online" (in which case it returns
91// CONNECTION_UNKNOWN) or "offline" (and then it returns CONNECTION_NONE).
92// This is challenging since the only thing we can test with certainty is
93// whether a *particular* host is reachable.
94//
95// While we can't conclusively determine when a user is "online", we can at
96// least reliably recognize some of the situtations when they are clearly
97// "offline". For example, if the user's laptop is not plugged into an ethernet
98// network and is not connected to any wireless networks, it must be offline.
99//
100// There are a number of different ways to implement this on Windows, each with
101// their pros and cons. Here is a comparison of various techniques considered:
102//
103// (1) Use InternetGetConnectedState (wininet.dll). This function is really easy
104// to use (literally a one-liner), and runs quickly. The drawback is it adds a
105// dependency on the wininet DLL.
106//
107// (2) Enumerate all of the network interfaces using GetAdaptersAddresses
108// (iphlpapi.dll), and assume we are "online" if there is at least one interface
109// that is connected, and that interface is not a loopback or tunnel.
110//
111// Safari on Windows has a fairly simple implementation that does this:
112// http://trac.webkit.org/browser/trunk/WebCore/platform/network/win/NetworkStateNotifierWin.cpp.
113//
114// Mozilla similarly uses this approach:
115// http://mxr.mozilla.org/mozilla1.9.2/source/netwerk/system/win32/nsNotifyAddrListener.cpp
116//
117// The biggest drawback to this approach is it is quite complicated.
118// WebKit's implementation for example doesn't seem to test for ICS gateways
119// (internet connection sharing), whereas Mozilla's implementation has extra
120// code to guess that.
121//
122// (3) The method used in this file comes from google talk, and is similar to
123// method (2). The main difference is it enumerates the winsock namespace
124// providers rather than the actual adapters.
125//
126// I ran some benchmarks comparing the performance of each on my Windows 7
127// workstation. Here is what I found:
128//   * Approach (1) was pretty much zero-cost after the initial call.
129//   * Approach (2) took an average of 3.25 milliseconds to enumerate the
130//     adapters.
131//   * Approach (3) took an average of 0.8 ms to enumerate the providers.
132//
133// In terms of correctness, all three approaches were comparable for the simple
134// experiments I ran... However none of them correctly returned "offline" when
135// executing 'ipconfig /release'.
136//
137NetworkChangeNotifier::ConnectionType
138NetworkChangeNotifierWin::RecomputeCurrentConnectionType() const {
139  DCHECK(CalledOnValidThread());
140
141  EnsureWinsockInit();
142
143  // The following code was adapted from:
144  // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/net/notifier/base/win/async_network_alive_win32.cc?view=markup&pathrev=47343
145  // The main difference is we only call WSALookupServiceNext once, whereas
146  // the earlier code would traverse the entire list and pass LUP_FLUSHPREVIOUS
147  // to skip past the large results.
148
149  HANDLE ws_handle;
150  WSAQUERYSET query_set = {0};
151  query_set.dwSize = sizeof(WSAQUERYSET);
152  query_set.dwNameSpace = NS_NLA;
153  // Initiate a client query to iterate through the
154  // currently connected networks.
155  if (0 != WSALookupServiceBegin(&query_set, LUP_RETURN_ALL,
156                                 &ws_handle)) {
157    LOG(ERROR) << "WSALookupServiceBegin failed with: " << WSAGetLastError();
158    return NetworkChangeNotifier::CONNECTION_UNKNOWN;
159  }
160
161  bool found_connection = false;
162
163  // Retrieve the first available network. In this function, we only
164  // need to know whether or not there is network connection.
165  // Allocate 256 bytes for name, it should be enough for most cases.
166  // If the name is longer, it is OK as we will check the code returned and
167  // set correct network status.
168  char result_buffer[sizeof(WSAQUERYSET) + 256] = {0};
169  DWORD length = sizeof(result_buffer);
170  reinterpret_cast<WSAQUERYSET*>(&result_buffer[0])->dwSize =
171      sizeof(WSAQUERYSET);
172  int result = WSALookupServiceNext(
173      ws_handle,
174      LUP_RETURN_NAME,
175      &length,
176      reinterpret_cast<WSAQUERYSET*>(&result_buffer[0]));
177
178  if (result == 0) {
179    // Found a connection!
180    found_connection = true;
181  } else {
182    DCHECK_EQ(SOCKET_ERROR, result);
183    result = WSAGetLastError();
184
185    // Error code WSAEFAULT means there is a network connection but the
186    // result_buffer size is too small to contain the results. The
187    // variable "length" returned from WSALookupServiceNext is the minimum
188    // number of bytes required. We do not need to retrieve detail info,
189    // it is enough knowing there was a connection.
190    if (result == WSAEFAULT) {
191      found_connection = true;
192    } else if (result == WSA_E_NO_MORE || result == WSAENOMORE) {
193      // There was nothing to iterate over!
194    } else {
195      LOG(WARNING) << "WSALookupServiceNext() failed with:" << result;
196    }
197  }
198
199  result = WSALookupServiceEnd(ws_handle);
200  LOG_IF(ERROR, result != 0)
201      << "WSALookupServiceEnd() failed with: " << result;
202
203  // TODO(droger): Return something more detailed than CONNECTION_UNKNOWN.
204  return found_connection ? NetworkChangeNotifier::CONNECTION_UNKNOWN :
205                            NetworkChangeNotifier::CONNECTION_NONE;
206}
207
208NetworkChangeNotifier::ConnectionType
209NetworkChangeNotifierWin::GetCurrentConnectionType() const {
210  base::AutoLock auto_lock(last_computed_connection_type_lock_);
211  return last_computed_connection_type_;
212}
213
214void NetworkChangeNotifierWin::SetCurrentConnectionType(
215    ConnectionType connection_type) {
216  base::AutoLock auto_lock(last_computed_connection_type_lock_);
217  last_computed_connection_type_ = connection_type;
218}
219
220void NetworkChangeNotifierWin::OnObjectSignaled(HANDLE object) {
221  DCHECK(CalledOnValidThread());
222  DCHECK(is_watching_);
223  is_watching_ = false;
224
225  // Start watching for the next address change.
226  WatchForAddressChange();
227
228  NotifyObservers();
229}
230
231void NetworkChangeNotifierWin::NotifyObservers() {
232  DCHECK(CalledOnValidThread());
233  SetCurrentConnectionType(RecomputeCurrentConnectionType());
234  NotifyObserversOfIPAddressChange();
235
236  // Calling GetConnectionType() at this very moment is likely to give
237  // the wrong result, so we delay that until a little bit later.
238  //
239  // The one second delay chosen here was determined experimentally
240  // by adamk on Windows 7.
241  // If after one second we determine we are still offline, we will
242  // delay again.
243  offline_polls_ = 0;
244  timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), this,
245               &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
246}
247
248void NetworkChangeNotifierWin::WatchForAddressChange() {
249  DCHECK(CalledOnValidThread());
250  DCHECK(!is_watching_);
251
252  // NotifyAddrChange occasionally fails with ERROR_OPEN_FAILED for unknown
253  // reasons.  More rarely, it's also been observed failing with
254  // ERROR_NO_SYSTEM_RESOURCES.  When either of these happens, we retry later.
255  if (!WatchForAddressChangeInternal()) {
256    ++sequential_failures_;
257
258    // TODO(mmenke):  If the UMA histograms indicate that this fixes
259    // http://crbug.com/69198, remove this histogram and consider reducing the
260    // retry interval.
261    if (sequential_failures_ == 2000) {
262      UMA_HISTOGRAM_COUNTS_10000("Net.NotifyAddrChangeFailures",
263                                 sequential_failures_);
264    }
265
266    base::MessageLoop::current()->PostDelayedTask(
267        FROM_HERE,
268        base::Bind(&NetworkChangeNotifierWin::WatchForAddressChange,
269                   weak_factory_.GetWeakPtr()),
270        base::TimeDelta::FromMilliseconds(
271            kWatchForAddressChangeRetryIntervalMs));
272    return;
273  }
274
275  // Treat the transition from NotifyAddrChange failing to succeeding as a
276  // network change event, since network changes were not being observed in
277  // that interval.
278  if (sequential_failures_ > 0)
279    NotifyObservers();
280
281  if (sequential_failures_ < 2000) {
282    UMA_HISTOGRAM_COUNTS_10000("Net.NotifyAddrChangeFailures",
283                               sequential_failures_);
284  }
285
286  is_watching_ = true;
287  sequential_failures_ = 0;
288}
289
290bool NetworkChangeNotifierWin::WatchForAddressChangeInternal() {
291  DCHECK(CalledOnValidThread());
292
293  if (!dns_config_service_thread_->IsRunning()) {
294    dns_config_service_thread_->StartWithOptions(
295      base::Thread::Options(base::MessageLoop::TYPE_IO, 0));
296  }
297
298  HANDLE handle = NULL;
299  DWORD ret = NotifyAddrChange(&handle, &addr_overlapped_);
300  if (ret != ERROR_IO_PENDING)
301    return false;
302
303  addr_watcher_.StartWatching(addr_overlapped_.hEvent, this);
304  return true;
305}
306
307void NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange() {
308  SetCurrentConnectionType(RecomputeCurrentConnectionType());
309  bool current_offline = IsOffline();
310  offline_polls_++;
311  // If we continue to appear offline, delay sending out the notification in
312  // case we appear to go online within 20 seconds.  UMA histogram data shows
313  // we may not detect the transition to online state after 1 second but within
314  // 20 seconds we generally do.
315  if (last_announced_offline_ && current_offline && offline_polls_ <= 20) {
316    timer_.Start(FROM_HERE, base::TimeDelta::FromSeconds(1), this,
317                 &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
318    return;
319  }
320  if (last_announced_offline_)
321    UMA_HISTOGRAM_CUSTOM_COUNTS("NCN.OfflinePolls", offline_polls_, 1, 50, 50);
322  last_announced_offline_ = current_offline;
323
324  NotifyObserversOfConnectionTypeChange();
325}
326
327}  // namespace net
328