1// Copyright (c) 2010 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/logging.h"
11#include "base/time.h"
12#include "net/base/winsock_init.h"
13
14#pragma comment(lib, "iphlpapi.lib")
15
16namespace net {
17
18NetworkChangeNotifierWin::NetworkChangeNotifierWin() {
19  memset(&addr_overlapped_, 0, sizeof addr_overlapped_);
20  addr_overlapped_.hEvent = WSACreateEvent();
21  WatchForAddressChange();
22}
23
24NetworkChangeNotifierWin::~NetworkChangeNotifierWin() {
25  CancelIPChangeNotify(&addr_overlapped_);
26  addr_watcher_.StopWatching();
27  WSACloseEvent(addr_overlapped_.hEvent);
28}
29
30// Conceptually we would like to tell whether the user is "online" verus
31// "offline".  This is challenging since the only thing we can test with
32// certainty is whether a *particular* host is reachable.
33//
34// While we can't conclusively determine when a user is "online", we can at
35// least reliably recognize some of the situtations when they are clearly
36// "offline". For example, if the user's laptop is not plugged into an ethernet
37// network and is not connected to any wireless networks, it must be offline.
38//
39// There are a number of different ways to implement this on Windows, each with
40// their pros and cons. Here is a comparison of various techniques considered:
41//
42// (1) Use InternetGetConnectedState (wininet.dll). This function is really easy
43// to use (literally a one-liner), and runs quickly. The drawback is it adds a
44// dependency on the wininet DLL.
45//
46// (2) Enumerate all of the network interfaces using GetAdaptersAddresses
47// (iphlpapi.dll), and assume we are "online" if there is at least one interface
48// that is connected, and that interface is not a loopback or tunnel.
49//
50// Safari on Windows has a fairly simple implementation that does this:
51// http://trac.webkit.org/browser/trunk/WebCore/platform/network/win/NetworkStateNotifierWin.cpp.
52//
53// Mozilla similarly uses this approach:
54// http://mxr.mozilla.org/mozilla1.9.2/source/netwerk/system/win32/nsNotifyAddrListener.cpp
55//
56// The biggest drawback to this approach is it is quite complicated.
57// WebKit's implementation for example doesn't seem to test for ICS gateways
58// (internet connection sharing), whereas Mozilla's implementation has extra
59// code to guess that.
60//
61// (3) The method used in this file comes from google talk, and is similar to
62// method (2). The main difference is it enumerates the winsock namespace
63// providers rather than the actual adapters.
64//
65// I ran some benchmarks comparing the performance of each on my Windows 7
66// workstation. Here is what I found:
67//   * Approach (1) was pretty much zero-cost after the initial call.
68//   * Approach (2) took an average of 3.25 milliseconds to enumerate the
69//     adapters.
70//   * Approach (3) took an average of 0.8 ms to enumerate the providers.
71//
72// In terms of correctness, all three approaches were comparable for the simple
73// experiments I ran... However none of them correctly returned "offline" when
74// executing 'ipconfig /release'.
75//
76bool NetworkChangeNotifierWin::IsCurrentlyOffline() const {
77
78  // TODO(eroman): We could cache this value, and only re-calculate it on
79  //               network changes. For now we recompute it each time asked,
80  //               since it is relatively fast (sub 1ms) and not called often.
81
82  EnsureWinsockInit();
83
84  // The following code was adapted from:
85  // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/net/notifier/base/win/async_network_alive_win32.cc?view=markup&pathrev=47343
86  // The main difference is we only call WSALookupServiceNext once, whereas
87  // the earlier code would traverse the entire list and pass LUP_FLUSHPREVIOUS
88  // to skip past the large results.
89
90  HANDLE ws_handle;
91  WSAQUERYSET query_set = {0};
92  query_set.dwSize = sizeof(WSAQUERYSET);
93  query_set.dwNameSpace = NS_NLA;
94  // Initiate a client query to iterate through the
95  // currently connected networks.
96  if (0 != WSALookupServiceBegin(&query_set, LUP_RETURN_ALL,
97                                 &ws_handle)) {
98    LOG(ERROR) << "WSALookupServiceBegin failed with: " << WSAGetLastError();
99    return false;
100  }
101
102  bool found_connection = false;
103
104  // Retrieve the first available network. In this function, we only
105  // need to know whether or not there is network connection.
106  // Allocate 256 bytes for name, it should be enough for most cases.
107  // If the name is longer, it is OK as we will check the code returned and
108  // set correct network status.
109  char result_buffer[sizeof(WSAQUERYSET) + 256] = {0};
110  DWORD length = sizeof(result_buffer);
111  reinterpret_cast<WSAQUERYSET*>(&result_buffer[0])->dwSize =
112      sizeof(WSAQUERYSET);
113  int result = WSALookupServiceNext(
114      ws_handle,
115      LUP_RETURN_NAME,
116      &length,
117      reinterpret_cast<WSAQUERYSET*>(&result_buffer[0]));
118
119  if (result == 0) {
120    // Found a connection!
121    found_connection = true;
122  } else {
123    DCHECK_EQ(SOCKET_ERROR, result);
124    result = WSAGetLastError();
125
126    // Error code WSAEFAULT means there is a network connection but the
127    // result_buffer size is too small to contain the results. The
128    // variable "length" returned from WSALookupServiceNext is the minimum
129    // number of bytes required. We do not need to retrieve detail info,
130    // it is enough knowing there was a connection.
131    if (result == WSAEFAULT) {
132      found_connection = true;
133    } else if (result == WSA_E_NO_MORE || result == WSAENOMORE) {
134      // There was nothing to iterate over!
135    } else {
136      LOG(WARNING) << "WSALookupServiceNext() failed with:" << result;
137    }
138  }
139
140  result = WSALookupServiceEnd(ws_handle);
141  LOG_IF(ERROR, result != 0)
142      << "WSALookupServiceEnd() failed with: " << result;
143
144  return !found_connection;
145}
146
147void NetworkChangeNotifierWin::OnObjectSignaled(HANDLE object) {
148  NotifyObserversOfIPAddressChange();
149
150  // Calling IsOffline() at this very moment is likely to give
151  // the wrong result, so we delay that until a little bit later.
152  //
153  // The one second delay chosen here was determined experimentally
154  // by adamk on Windows 7.
155  timer_.Stop();  // cancel any already waiting notification
156  timer_.Start(base::TimeDelta::FromSeconds(1), this,
157               &NetworkChangeNotifierWin::NotifyParentOfOnlineStateChange);
158
159  // Start watching for the next address change.
160  WatchForAddressChange();
161}
162
163void NetworkChangeNotifierWin::WatchForAddressChange() {
164  HANDLE handle = NULL;
165  DWORD ret = NotifyAddrChange(&handle, &addr_overlapped_);
166  CHECK(ret == ERROR_IO_PENDING);
167  addr_watcher_.StartWatching(addr_overlapped_.hEvent, this);
168}
169
170void NetworkChangeNotifierWin::NotifyParentOfOnlineStateChange() {
171  NotifyObserversOfOnlineStateChange();
172}
173
174}  // namespace net
175