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