• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_win.h"
6 
7 #include <winsock2.h>
8 
9 #include <iphlpapi.h>
10 
11 #include <utility>
12 
13 #include "base/feature_list.h"
14 #include "base/functional/bind.h"
15 #include "base/location.h"
16 #include "base/logging.h"
17 #include "base/metrics/histogram_macros.h"
18 #include "base/task/sequenced_task_runner.h"
19 #include "base/task/single_thread_task_runner.h"
20 #include "base/task/task_traits.h"
21 #include "base/task/thread_pool.h"
22 #include "base/threading/thread.h"
23 #include "base/time/time.h"
24 #include "base/win/windows_version.h"
25 #include "net/base/features.h"
26 #include "net/base/network_cost_change_notifier_win.h"
27 #include "net/base/winsock_init.h"
28 #include "net/base/winsock_util.h"
29 
30 namespace net {
31 
32 namespace {
33 
34 // Time between NotifyAddrChange retries, on failure.
35 const int kWatchForAddressChangeRetryIntervalMs = 500;
36 
37 }  // namespace
38 
NetworkChangeNotifierWin()39 NetworkChangeNotifierWin::NetworkChangeNotifierWin()
40     : NetworkChangeNotifier(NetworkChangeCalculatorParamsWin()),
41       blocking_task_runner_(
42           base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
43       last_computed_connection_type_(RecomputeCurrentConnectionType()),
44       last_announced_offline_(last_computed_connection_type_ ==
45                               CONNECTION_NONE),
46       sequence_runner_for_registration_(
47           base::SequencedTaskRunner::GetCurrentDefault()) {
48   memset(&addr_overlapped_, 0, sizeof addr_overlapped_);
49   addr_overlapped_.hEvent = WSACreateEvent();
50 
51   cost_change_notifier_ = NetworkCostChangeNotifierWin::CreateInstance(
52       base::BindRepeating(&NetworkChangeNotifierWin::OnCostChanged,
53                           weak_factory_.GetWeakPtr()));
54 }
55 
~NetworkChangeNotifierWin()56 NetworkChangeNotifierWin::~NetworkChangeNotifierWin() {
57   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
58   ClearGlobalPointer();
59   if (is_watching_) {
60     CancelIPChangeNotify(&addr_overlapped_);
61     addr_watcher_.StopWatching();
62   }
63   WSACloseEvent(addr_overlapped_.hEvent);
64 }
65 
66 // static
67 NetworkChangeNotifier::NetworkChangeCalculatorParams
NetworkChangeCalculatorParamsWin()68 NetworkChangeNotifierWin::NetworkChangeCalculatorParamsWin() {
69   NetworkChangeCalculatorParams params;
70   // Delay values arrived at by simple experimentation and adjusted so as to
71   // produce a single signal when switching between network connections.
72   params.ip_address_offline_delay_ = base::Milliseconds(1500);
73   params.ip_address_online_delay_ = base::Milliseconds(1500);
74   params.connection_type_offline_delay_ = base::Milliseconds(1500);
75   params.connection_type_online_delay_ = base::Milliseconds(500);
76   return params;
77 }
78 
79 // static
80 NetworkChangeNotifier::ConnectionType
RecomputeCurrentConnectionTypeModern()81 NetworkChangeNotifierWin::RecomputeCurrentConnectionTypeModern() {
82   using GetNetworkConnectivityHintType =
83       decltype(&::GetNetworkConnectivityHint);
84 
85   // This API is only available on Windows 10 Build 19041. However, it works
86   // inside the Network Service Sandbox, so is preferred. See
87   GetNetworkConnectivityHintType get_network_connectivity_hint =
88       reinterpret_cast<GetNetworkConnectivityHintType>(::GetProcAddress(
89           ::GetModuleHandleA("iphlpapi.dll"), "GetNetworkConnectivityHint"));
90   if (!get_network_connectivity_hint) {
91     return NetworkChangeNotifier::CONNECTION_UNKNOWN;
92   }
93   NL_NETWORK_CONNECTIVITY_HINT hint;
94   // https://learn.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getnetworkconnectivityhint.
95   auto ret = get_network_connectivity_hint(&hint);
96   if (ret != NO_ERROR) {
97     return NetworkChangeNotifier::CONNECTION_UNKNOWN;
98   }
99 
100   switch (hint.ConnectivityLevel) {
101     case NetworkConnectivityLevelHintUnknown:
102       return NetworkChangeNotifier::CONNECTION_UNKNOWN;
103     case NetworkConnectivityLevelHintNone:
104     case NetworkConnectivityLevelHintHidden:
105       return NetworkChangeNotifier::CONNECTION_NONE;
106     case NetworkConnectivityLevelHintLocalAccess:
107     case NetworkConnectivityLevelHintInternetAccess:
108     case NetworkConnectivityLevelHintConstrainedInternetAccess:
109       // TODO(droger): Return something more detailed than CONNECTION_UNKNOWN.
110       return ConnectionTypeFromInterfaces();
111   }
112 
113   NOTREACHED();
114 }
115 
116 // This implementation does not return the actual connection type but merely
117 // determines if the user is "online" (in which case it returns
118 // CONNECTION_UNKNOWN) or "offline" (and then it returns CONNECTION_NONE).
119 // This is challenging since the only thing we can test with certainty is
120 // whether a *particular* host is reachable.
121 //
122 // While we can't conclusively determine when a user is "online", we can at
123 // least reliably recognize some of the situtations when they are clearly
124 // "offline". For example, if the user's laptop is not plugged into an ethernet
125 // network and is not connected to any wireless networks, it must be offline.
126 //
127 // There are a number of different ways to implement this on Windows, each with
128 // their pros and cons. Here is a comparison of various techniques considered:
129 //
130 // (1) Use InternetGetConnectedState (wininet.dll). This function is really easy
131 // to use (literally a one-liner), and runs quickly. The drawback is it adds a
132 // dependency on the wininet DLL.
133 //
134 // (2) Enumerate all of the network interfaces using GetAdaptersAddresses
135 // (iphlpapi.dll), and assume we are "online" if there is at least one interface
136 // that is connected, and that interface is not a loopback or tunnel.
137 //
138 // Safari on Windows has a fairly simple implementation that does this:
139 // http://trac.webkit.org/browser/trunk/WebCore/platform/network/win/NetworkStateNotifierWin.cpp.
140 //
141 // Mozilla similarly uses this approach:
142 // http://mxr.mozilla.org/mozilla1.9.2/source/netwerk/system/win32/nsNotifyAddrListener.cpp
143 //
144 // The biggest drawback to this approach is it is quite complicated.
145 // WebKit's implementation for example doesn't seem to test for ICS gateways
146 // (internet connection sharing), whereas Mozilla's implementation has extra
147 // code to guess that.
148 //
149 // (3) The method used in this file comes from google talk, and is similar to
150 // method (2). The main difference is it enumerates the winsock namespace
151 // providers rather than the actual adapters.
152 //
153 // I ran some benchmarks comparing the performance of each on my Windows 7
154 // workstation. Here is what I found:
155 //   * Approach (1) was pretty much zero-cost after the initial call.
156 //   * Approach (2) took an average of 3.25 milliseconds to enumerate the
157 //     adapters.
158 //   * Approach (3) took an average of 0.8 ms to enumerate the providers.
159 //
160 // In terms of correctness, all three approaches were comparable for the simple
161 // experiments I ran... However none of them correctly returned "offline" when
162 // executing 'ipconfig /release'.
163 //
164 // static
165 NetworkChangeNotifier::ConnectionType
RecomputeCurrentConnectionType()166 NetworkChangeNotifierWin::RecomputeCurrentConnectionType() {
167   if (base::win::GetVersion() >= base::win::Version::WIN10_20H1 &&
168       base::FeatureList::IsEnabled(
169           features::kEnableGetNetworkConnectivityHintAPI)) {
170     return RecomputeCurrentConnectionTypeModern();
171   }
172 
173   EnsureWinsockInit();
174 
175   // The following code was adapted from:
176   // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/net/notifier/base/win/async_network_alive_win32.cc?view=markup&pathrev=47343
177   // The main difference is we only call WSALookupServiceNext once, whereas
178   // the earlier code would traverse the entire list and pass LUP_FLUSHPREVIOUS
179   // to skip past the large results.
180 
181   HANDLE ws_handle;
182   WSAQUERYSET query_set = {0};
183   query_set.dwSize = sizeof(WSAQUERYSET);
184   query_set.dwNameSpace = NS_NLA;
185   // Initiate a client query to iterate through the
186   // currently connected networks.
187   if (0 != WSALookupServiceBegin(&query_set, LUP_RETURN_ALL, &ws_handle)) {
188     LOG(ERROR) << "WSALookupServiceBegin failed with: " << WSAGetLastError();
189     return NetworkChangeNotifier::CONNECTION_UNKNOWN;
190   }
191 
192   bool found_connection = false;
193 
194   // Retrieve the first available network. In this function, we only
195   // need to know whether or not there is network connection.
196   // Allocate 256 bytes for name, it should be enough for most cases.
197   // If the name is longer, it is OK as we will check the code returned and
198   // set correct network status.
199   char result_buffer[sizeof(WSAQUERYSET) + 256] = {0};
200   DWORD length = sizeof(result_buffer);
201   reinterpret_cast<WSAQUERYSET*>(&result_buffer[0])->dwSize =
202       sizeof(WSAQUERYSET);
203   int result =
204       WSALookupServiceNext(ws_handle, LUP_RETURN_NAME, &length,
205                            reinterpret_cast<WSAQUERYSET*>(&result_buffer[0]));
206 
207   if (result == 0) {
208     // Found a connection!
209     found_connection = true;
210   } else {
211     DCHECK_EQ(SOCKET_ERROR, result);
212     result = WSAGetLastError();
213 
214     // Error code WSAEFAULT means there is a network connection but the
215     // result_buffer size is too small to contain the results. The
216     // variable "length" returned from WSALookupServiceNext is the minimum
217     // number of bytes required. We do not need to retrieve detail info,
218     // it is enough knowing there was a connection.
219     if (result == WSAEFAULT) {
220       found_connection = true;
221     } else if (result == WSA_E_NO_MORE || result == WSAENOMORE) {
222       // There was nothing to iterate over!
223     } else {
224       LOG(WARNING) << "WSALookupServiceNext() failed with:" << result;
225     }
226   }
227 
228   result = WSALookupServiceEnd(ws_handle);
229   LOG_IF(ERROR, result != 0) << "WSALookupServiceEnd() failed with: " << result;
230 
231   // TODO(droger): Return something more detailed than CONNECTION_UNKNOWN.
232   return found_connection ? ConnectionTypeFromInterfaces()
233                           : NetworkChangeNotifier::CONNECTION_NONE;
234 }
235 
RecomputeCurrentConnectionTypeOnBlockingSequence(base::OnceCallback<void (ConnectionType)> reply_callback) const236 void NetworkChangeNotifierWin::RecomputeCurrentConnectionTypeOnBlockingSequence(
237     base::OnceCallback<void(ConnectionType)> reply_callback) const {
238   // Unretained is safe in this call because this object owns the thread and the
239   // thread is stopped in this object's destructor.
240   blocking_task_runner_->PostTaskAndReplyWithResult(
241       FROM_HERE,
242       base::BindOnce(&NetworkChangeNotifierWin::RecomputeCurrentConnectionType),
243       std::move(reply_callback));
244 }
245 
246 NetworkChangeNotifier::ConnectionCost
GetCurrentConnectionCost()247 NetworkChangeNotifierWin::GetCurrentConnectionCost() {
248   if (last_computed_connection_cost_ ==
249       ConnectionCost::CONNECTION_COST_UNKNOWN) {
250     // Use the default logic when the Windows OS APIs do not have a cost for the
251     // current connection.
252     return NetworkChangeNotifier::GetCurrentConnectionCost();
253   }
254   return last_computed_connection_cost_;
255 }
256 
OnCostChanged(NetworkChangeNotifier::ConnectionCost new_cost)257 void NetworkChangeNotifierWin::OnCostChanged(
258     NetworkChangeNotifier::ConnectionCost new_cost) {
259   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
260 
261   // Only notify if there's actually a change.
262   if (last_computed_connection_cost_ != new_cost) {
263     last_computed_connection_cost_ = new_cost;
264     NotifyObserversOfConnectionCostChange();
265   }
266 }
267 
268 NetworkChangeNotifier::ConnectionType
GetCurrentConnectionType() const269 NetworkChangeNotifierWin::GetCurrentConnectionType() const {
270   base::AutoLock auto_lock(last_computed_connection_type_lock_);
271   return last_computed_connection_type_;
272 }
273 
SetCurrentConnectionType(ConnectionType connection_type)274 void NetworkChangeNotifierWin::SetCurrentConnectionType(
275     ConnectionType connection_type) {
276   base::AutoLock auto_lock(last_computed_connection_type_lock_);
277   last_computed_connection_type_ = connection_type;
278 }
279 
OnObjectSignaled(HANDLE object)280 void NetworkChangeNotifierWin::OnObjectSignaled(HANDLE object) {
281   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
282   DCHECK(is_watching_);
283   is_watching_ = false;
284 
285   // Start watching for the next address change.
286   WatchForAddressChange();
287 
288   RecomputeCurrentConnectionTypeOnBlockingSequence(base::BindOnce(
289       &NetworkChangeNotifierWin::NotifyObservers, weak_factory_.GetWeakPtr()));
290 }
291 
NotifyObservers(ConnectionType connection_type)292 void NetworkChangeNotifierWin::NotifyObservers(ConnectionType connection_type) {
293   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
294   SetCurrentConnectionType(connection_type);
295   NotifyObserversOfIPAddressChange();
296 
297   // Calling GetConnectionType() at this very moment is likely to give
298   // the wrong result, so we delay that until a little bit later.
299   //
300   // The one second delay chosen here was determined experimentally
301   // by adamk on Windows 7.
302   // If after one second we determine we are still offline, we will
303   // delay again.
304   offline_polls_ = 0;
305   timer_.Start(FROM_HERE, base::Seconds(1), this,
306                &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
307 }
308 
WatchForAddressChange()309 void NetworkChangeNotifierWin::WatchForAddressChange() {
310   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
311   DCHECK(!is_watching_);
312 
313   // NotifyAddrChange occasionally fails with ERROR_OPEN_FAILED for unknown
314   // reasons.  More rarely, it's also been observed failing with
315   // ERROR_NO_SYSTEM_RESOURCES.  When either of these happens, we retry later.
316   if (!WatchForAddressChangeInternal()) {
317     ++sequential_failures_;
318 
319     base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
320         FROM_HERE,
321         base::BindOnce(&NetworkChangeNotifierWin::WatchForAddressChange,
322                        weak_factory_.GetWeakPtr()),
323         base::Milliseconds(kWatchForAddressChangeRetryIntervalMs));
324     return;
325   }
326 
327   // Treat the transition from NotifyAddrChange failing to succeeding as a
328   // network change event, since network changes were not being observed in
329   // that interval.
330   if (sequential_failures_ > 0) {
331     RecomputeCurrentConnectionTypeOnBlockingSequence(
332         base::BindOnce(&NetworkChangeNotifierWin::NotifyObservers,
333                        weak_factory_.GetWeakPtr()));
334   }
335 
336   is_watching_ = true;
337   sequential_failures_ = 0;
338 }
339 
WatchForAddressChangeInternal()340 bool NetworkChangeNotifierWin::WatchForAddressChangeInternal() {
341   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
342 
343   ResetEventIfSignaled(addr_overlapped_.hEvent);
344   HANDLE handle = nullptr;
345   DWORD ret = NotifyAddrChange(&handle, &addr_overlapped_);
346   if (ret != ERROR_IO_PENDING)
347     return false;
348 
349   addr_watcher_.StartWatchingOnce(addr_overlapped_.hEvent, this);
350   return true;
351 }
352 
NotifyParentOfConnectionTypeChange()353 void NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange() {
354   RecomputeCurrentConnectionTypeOnBlockingSequence(base::BindOnce(
355       &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChangeImpl,
356       weak_factory_.GetWeakPtr()));
357 }
358 
NotifyParentOfConnectionTypeChangeImpl(ConnectionType connection_type)359 void NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChangeImpl(
360     ConnectionType connection_type) {
361   SetCurrentConnectionType(connection_type);
362   bool current_offline = IsOffline();
363   offline_polls_++;
364   // If we continue to appear offline, delay sending out the notification in
365   // case we appear to go online within 20 seconds.  UMA histogram data shows
366   // we may not detect the transition to online state after 1 second but within
367   // 20 seconds we generally do.
368   if (last_announced_offline_ && current_offline && offline_polls_ <= 20) {
369     timer_.Start(FROM_HERE, base::Seconds(1), this,
370                  &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
371     return;
372   }
373   if (last_announced_offline_)
374     UMA_HISTOGRAM_CUSTOM_COUNTS("NCN.OfflinePolls", offline_polls_, 1, 50, 50);
375   last_announced_offline_ = current_offline;
376 
377   NotifyObserversOfConnectionTypeChange();
378   double max_bandwidth_mbps = 0.0;
379   ConnectionType max_connection_type = CONNECTION_NONE;
380   GetCurrentMaxBandwidthAndConnectionType(&max_bandwidth_mbps,
381                                           &max_connection_type);
382   NotifyObserversOfMaxBandwidthChange(max_bandwidth_mbps, max_connection_type);
383 }
384 
385 }  // namespace net
386