• 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 <iphlpapi.h>
8 #include <winsock2.h>
9 
10 #include <utility>
11 
12 #include "base/functional/bind.h"
13 #include "base/location.h"
14 #include "base/logging.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/task/sequenced_task_runner.h"
17 #include "base/task/single_thread_task_runner.h"
18 #include "base/task/task_traits.h"
19 #include "base/task/thread_pool.h"
20 #include "base/threading/thread.h"
21 #include "base/time/time.h"
22 #include "net/base/winsock_init.h"
23 #include "net/base/winsock_util.h"
24 
25 namespace net {
26 
27 namespace {
28 
29 // Time between NotifyAddrChange retries, on failure.
30 const int kWatchForAddressChangeRetryIntervalMs = 500;
31 
GetConnectionPoints(IUnknown * manager,REFIID IIDSyncInterface,IConnectionPoint ** connection_point_raw)32 HRESULT GetConnectionPoints(IUnknown* manager,
33                             REFIID IIDSyncInterface,
34                             IConnectionPoint** connection_point_raw) {
35   *connection_point_raw = nullptr;
36   Microsoft::WRL::ComPtr<IConnectionPointContainer> connection_point_container;
37   HRESULT hr =
38       manager->QueryInterface(IID_PPV_ARGS(&connection_point_container));
39   if (FAILED(hr))
40     return hr;
41 
42   // Find the interface
43   Microsoft::WRL::ComPtr<IConnectionPoint> connection_point;
44   hr = connection_point_container->FindConnectionPoint(IIDSyncInterface,
45                                                        &connection_point);
46   if (FAILED(hr))
47     return hr;
48 
49   *connection_point_raw = connection_point.Get();
50   (*connection_point_raw)->AddRef();
51 
52   return hr;
53 }
54 
55 }  // namespace
56 
57 // This class is used as an event sink to register for notifications from the
58 // INetworkCostManagerEvents interface. In particular, we are focused on getting
59 // notified when the Connection Cost changes. This is only supported on Win10+.
60 class NetworkCostManagerEventSink
61     : public Microsoft::WRL::RuntimeClass<
62           Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
63           INetworkCostManagerEvents> {
64  public:
65   using CostChangedCallback = base::RepeatingCallback<void()>;
66 
NetworkCostManagerEventSink(INetworkCostManager * cost_manager,const CostChangedCallback & callback)67   NetworkCostManagerEventSink(INetworkCostManager* cost_manager,
68                               const CostChangedCallback& callback)
69       : network_cost_manager_(cost_manager), cost_changed_callback_(callback) {}
70   ~NetworkCostManagerEventSink() override = default;
71 
72   // INetworkCostManagerEvents members
CostChanged(_In_ DWORD cost,_In_opt_ NLM_SOCKADDR *)73   IFACEMETHODIMP CostChanged(_In_ DWORD cost,
74                              _In_opt_ NLM_SOCKADDR* /*pSockAddr*/) override {
75     cost_changed_callback_.Run();
76     return S_OK;
77   }
78 
DataPlanStatusChanged(_In_opt_ NLM_SOCKADDR *)79   IFACEMETHODIMP DataPlanStatusChanged(
80       _In_opt_ NLM_SOCKADDR* /*pSockAddr*/) override {
81     return S_OK;
82   }
83 
RegisterForNotifications()84   HRESULT RegisterForNotifications() {
85     Microsoft::WRL::ComPtr<IUnknown> unknown;
86     HRESULT hr = QueryInterface(IID_PPV_ARGS(&unknown));
87     if (hr != S_OK)
88       return hr;
89 
90     hr = GetConnectionPoints(network_cost_manager_.Get(),
91                              IID_INetworkCostManagerEvents, &connection_point_);
92     if (hr != S_OK)
93       return hr;
94 
95     hr = connection_point_->Advise(unknown.Get(), &cookie_);
96     return hr;
97   }
98 
UnRegisterForNotifications()99   void UnRegisterForNotifications() {
100     if (connection_point_) {
101       connection_point_->Unadvise(cookie_);
102       connection_point_ = nullptr;
103       cookie_ = 0;
104     }
105   }
106 
107  private:
108   Microsoft::WRL::ComPtr<INetworkCostManager> network_cost_manager_;
109   Microsoft::WRL::ComPtr<IConnectionPoint> connection_point_;
110   DWORD cookie_ = 0;
111   CostChangedCallback cost_changed_callback_;
112 };
113 
NetworkChangeNotifierWin()114 NetworkChangeNotifierWin::NetworkChangeNotifierWin()
115     : NetworkChangeNotifier(NetworkChangeCalculatorParamsWin()),
116       blocking_task_runner_(
117           base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()})),
118       last_computed_connection_type_(RecomputeCurrentConnectionType()),
119       last_announced_offline_(last_computed_connection_type_ ==
120                               CONNECTION_NONE),
121       sequence_runner_for_registration_(
122           base::SequencedTaskRunner::GetCurrentDefault()) {
123   memset(&addr_overlapped_, 0, sizeof addr_overlapped_);
124   addr_overlapped_.hEvent = WSACreateEvent();
125 }
126 
~NetworkChangeNotifierWin()127 NetworkChangeNotifierWin::~NetworkChangeNotifierWin() {
128   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
129   ClearGlobalPointer();
130   if (is_watching_) {
131     CancelIPChangeNotify(&addr_overlapped_);
132     addr_watcher_.StopWatching();
133   }
134   WSACloseEvent(addr_overlapped_.hEvent);
135 
136   if (network_cost_manager_event_sink_) {
137     network_cost_manager_event_sink_->UnRegisterForNotifications();
138     network_cost_manager_event_sink_ = nullptr;
139   }
140 }
141 
142 // static
143 NetworkChangeNotifier::NetworkChangeCalculatorParams
NetworkChangeCalculatorParamsWin()144 NetworkChangeNotifierWin::NetworkChangeCalculatorParamsWin() {
145   NetworkChangeCalculatorParams params;
146   // Delay values arrived at by simple experimentation and adjusted so as to
147   // produce a single signal when switching between network connections.
148   params.ip_address_offline_delay_ = base::Milliseconds(1500);
149   params.ip_address_online_delay_ = base::Milliseconds(1500);
150   params.connection_type_offline_delay_ = base::Milliseconds(1500);
151   params.connection_type_online_delay_ = base::Milliseconds(500);
152   return params;
153 }
154 
155 // This implementation does not return the actual connection type but merely
156 // determines if the user is "online" (in which case it returns
157 // CONNECTION_UNKNOWN) or "offline" (and then it returns CONNECTION_NONE).
158 // This is challenging since the only thing we can test with certainty is
159 // whether a *particular* host is reachable.
160 //
161 // While we can't conclusively determine when a user is "online", we can at
162 // least reliably recognize some of the situtations when they are clearly
163 // "offline". For example, if the user's laptop is not plugged into an ethernet
164 // network and is not connected to any wireless networks, it must be offline.
165 //
166 // There are a number of different ways to implement this on Windows, each with
167 // their pros and cons. Here is a comparison of various techniques considered:
168 //
169 // (1) Use InternetGetConnectedState (wininet.dll). This function is really easy
170 // to use (literally a one-liner), and runs quickly. The drawback is it adds a
171 // dependency on the wininet DLL.
172 //
173 // (2) Enumerate all of the network interfaces using GetAdaptersAddresses
174 // (iphlpapi.dll), and assume we are "online" if there is at least one interface
175 // that is connected, and that interface is not a loopback or tunnel.
176 //
177 // Safari on Windows has a fairly simple implementation that does this:
178 // http://trac.webkit.org/browser/trunk/WebCore/platform/network/win/NetworkStateNotifierWin.cpp.
179 //
180 // Mozilla similarly uses this approach:
181 // http://mxr.mozilla.org/mozilla1.9.2/source/netwerk/system/win32/nsNotifyAddrListener.cpp
182 //
183 // The biggest drawback to this approach is it is quite complicated.
184 // WebKit's implementation for example doesn't seem to test for ICS gateways
185 // (internet connection sharing), whereas Mozilla's implementation has extra
186 // code to guess that.
187 //
188 // (3) The method used in this file comes from google talk, and is similar to
189 // method (2). The main difference is it enumerates the winsock namespace
190 // providers rather than the actual adapters.
191 //
192 // I ran some benchmarks comparing the performance of each on my Windows 7
193 // workstation. Here is what I found:
194 //   * Approach (1) was pretty much zero-cost after the initial call.
195 //   * Approach (2) took an average of 3.25 milliseconds to enumerate the
196 //     adapters.
197 //   * Approach (3) took an average of 0.8 ms to enumerate the providers.
198 //
199 // In terms of correctness, all three approaches were comparable for the simple
200 // experiments I ran... However none of them correctly returned "offline" when
201 // executing 'ipconfig /release'.
202 //
203 // static
204 NetworkChangeNotifier::ConnectionType
RecomputeCurrentConnectionType()205 NetworkChangeNotifierWin::RecomputeCurrentConnectionType() {
206   EnsureWinsockInit();
207 
208   // The following code was adapted from:
209   // http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/net/notifier/base/win/async_network_alive_win32.cc?view=markup&pathrev=47343
210   // The main difference is we only call WSALookupServiceNext once, whereas
211   // the earlier code would traverse the entire list and pass LUP_FLUSHPREVIOUS
212   // to skip past the large results.
213 
214   HANDLE ws_handle;
215   WSAQUERYSET query_set = {0};
216   query_set.dwSize = sizeof(WSAQUERYSET);
217   query_set.dwNameSpace = NS_NLA;
218   // Initiate a client query to iterate through the
219   // currently connected networks.
220   if (0 != WSALookupServiceBegin(&query_set, LUP_RETURN_ALL, &ws_handle)) {
221     LOG(ERROR) << "WSALookupServiceBegin failed with: " << WSAGetLastError();
222     return NetworkChangeNotifier::CONNECTION_UNKNOWN;
223   }
224 
225   bool found_connection = false;
226 
227   // Retrieve the first available network. In this function, we only
228   // need to know whether or not there is network connection.
229   // Allocate 256 bytes for name, it should be enough for most cases.
230   // If the name is longer, it is OK as we will check the code returned and
231   // set correct network status.
232   char result_buffer[sizeof(WSAQUERYSET) + 256] = {0};
233   DWORD length = sizeof(result_buffer);
234   reinterpret_cast<WSAQUERYSET*>(&result_buffer[0])->dwSize =
235       sizeof(WSAQUERYSET);
236   int result =
237       WSALookupServiceNext(ws_handle, LUP_RETURN_NAME, &length,
238                            reinterpret_cast<WSAQUERYSET*>(&result_buffer[0]));
239 
240   if (result == 0) {
241     // Found a connection!
242     found_connection = true;
243   } else {
244     DCHECK_EQ(SOCKET_ERROR, result);
245     result = WSAGetLastError();
246 
247     // Error code WSAEFAULT means there is a network connection but the
248     // result_buffer size is too small to contain the results. The
249     // variable "length" returned from WSALookupServiceNext is the minimum
250     // number of bytes required. We do not need to retrieve detail info,
251     // it is enough knowing there was a connection.
252     if (result == WSAEFAULT) {
253       found_connection = true;
254     } else if (result == WSA_E_NO_MORE || result == WSAENOMORE) {
255       // There was nothing to iterate over!
256     } else {
257       LOG(WARNING) << "WSALookupServiceNext() failed with:" << result;
258     }
259   }
260 
261   result = WSALookupServiceEnd(ws_handle);
262   LOG_IF(ERROR, result != 0) << "WSALookupServiceEnd() failed with: " << result;
263 
264   // TODO(droger): Return something more detailed than CONNECTION_UNKNOWN.
265   return found_connection ? ConnectionTypeFromInterfaces()
266                           : NetworkChangeNotifier::CONNECTION_NONE;
267 }
268 
RecomputeCurrentConnectionTypeOnBlockingSequence(base::OnceCallback<void (ConnectionType)> reply_callback) const269 void NetworkChangeNotifierWin::RecomputeCurrentConnectionTypeOnBlockingSequence(
270     base::OnceCallback<void(ConnectionType)> reply_callback) const {
271   // Unretained is safe in this call because this object owns the thread and the
272   // thread is stopped in this object's destructor.
273   blocking_task_runner_->PostTaskAndReplyWithResult(
274       FROM_HERE,
275       base::BindOnce(&NetworkChangeNotifierWin::RecomputeCurrentConnectionType),
276       std::move(reply_callback));
277 }
278 
279 NetworkChangeNotifier::ConnectionCost
GetCurrentConnectionCost()280 NetworkChangeNotifierWin::GetCurrentConnectionCost() {
281   InitializeConnectionCost();
282 
283   // If we don't have the event sink we aren't registered for automatic updates.
284   // In that case, we need to update the value at the time it is requested.
285   if (!network_cost_manager_event_sink_)
286     UpdateConnectionCostFromCostManager();
287 
288   return last_computed_connection_cost_;
289 }
290 
InitializeConnectionCostOnce()291 bool NetworkChangeNotifierWin::InitializeConnectionCostOnce() {
292   HRESULT hr =
293       ::CoCreateInstance(CLSID_NetworkListManager, nullptr, CLSCTX_ALL,
294                          IID_INetworkCostManager, &network_cost_manager_);
295   if (FAILED(hr)) {
296     SetCurrentConnectionCost(CONNECTION_COST_UNKNOWN);
297     return true;
298   }
299 
300   UpdateConnectionCostFromCostManager();
301 
302   return true;
303 }
304 
InitializeConnectionCost()305 void NetworkChangeNotifierWin::InitializeConnectionCost() {
306   static bool g_connection_cost_initialized = InitializeConnectionCostOnce();
307   DCHECK(g_connection_cost_initialized);
308 }
309 
UpdateConnectionCostFromCostManager()310 HRESULT NetworkChangeNotifierWin::UpdateConnectionCostFromCostManager() {
311   if (!network_cost_manager_)
312     return E_ABORT;
313 
314   DWORD cost = NLM_CONNECTION_COST_UNKNOWN;
315   HRESULT hr = network_cost_manager_->GetCost(&cost, nullptr);
316   if (FAILED(hr)) {
317     SetCurrentConnectionCost(CONNECTION_COST_UNKNOWN);
318   } else {
319     SetCurrentConnectionCost(
320         ConnectionCostFromNlmCost((NLM_CONNECTION_COST)cost));
321   }
322   return hr;
323 }
324 
325 // static
326 NetworkChangeNotifier::ConnectionCost
ConnectionCostFromNlmCost(NLM_CONNECTION_COST cost)327 NetworkChangeNotifierWin::ConnectionCostFromNlmCost(NLM_CONNECTION_COST cost) {
328   if (cost == NLM_CONNECTION_COST_UNKNOWN)
329     return CONNECTION_COST_UNKNOWN;
330   else if ((cost & NLM_CONNECTION_COST_UNRESTRICTED) != 0)
331     return CONNECTION_COST_UNMETERED;
332   else
333     return CONNECTION_COST_METERED;
334 }
335 
SetCurrentConnectionCost(ConnectionCost connection_cost)336 void NetworkChangeNotifierWin::SetCurrentConnectionCost(
337     ConnectionCost connection_cost) {
338   last_computed_connection_cost_ = connection_cost;
339 }
340 
OnCostChanged()341 void NetworkChangeNotifierWin::OnCostChanged() {
342   ConnectionCost old_cost = last_computed_connection_cost_;
343   // It is possible to get multiple notifications in a short period of time.
344   // Rather than worrying about whether this notification represents the latest,
345   // just get the current value from the CostManager so we know that we're
346   // actually getting the correct value.
347   UpdateConnectionCostFromCostManager();
348   // Only notify if there's actually a change.
349   if (old_cost != GetCurrentConnectionCost())
350     NotifyObserversOfConnectionCostChange();
351 }
352 
ConnectionCostObserverAdded()353 void NetworkChangeNotifierWin::ConnectionCostObserverAdded() {
354   sequence_runner_for_registration_->PostTask(
355       FROM_HERE,
356       base::BindOnce(&NetworkChangeNotifierWin::OnConnectionCostObserverAdded,
357                      weak_factory_.GetWeakPtr()));
358 }
359 
OnConnectionCostObserverAdded()360 void NetworkChangeNotifierWin::OnConnectionCostObserverAdded() {
361   DCHECK(sequence_runner_for_registration_->RunsTasksInCurrentSequence());
362   InitializeConnectionCost();
363 
364   // No need to register if we don't have a cost manager or if we're already
365   // registered.
366   if (!network_cost_manager_ || network_cost_manager_event_sink_)
367     return;
368 
369   network_cost_manager_event_sink_ =
370       Microsoft::WRL::Make<net::NetworkCostManagerEventSink>(
371           network_cost_manager_.Get(),
372           base::BindRepeating(&NetworkChangeNotifierWin::OnCostChanged,
373                               weak_factory_.GetWeakPtr()));
374   HRESULT hr = network_cost_manager_event_sink_->RegisterForNotifications();
375   if (hr != S_OK) {
376     // If registration failed for any reason, just destroy the event sink. The
377     // observer will remain connected but will not receive any updates. If
378     // another observer gets added later, we can re-attempt registration.
379     network_cost_manager_event_sink_ = nullptr;
380   }
381 }
382 
383 NetworkChangeNotifier::ConnectionType
GetCurrentConnectionType() const384 NetworkChangeNotifierWin::GetCurrentConnectionType() const {
385   base::AutoLock auto_lock(last_computed_connection_type_lock_);
386   return last_computed_connection_type_;
387 }
388 
SetCurrentConnectionType(ConnectionType connection_type)389 void NetworkChangeNotifierWin::SetCurrentConnectionType(
390     ConnectionType connection_type) {
391   base::AutoLock auto_lock(last_computed_connection_type_lock_);
392   last_computed_connection_type_ = connection_type;
393 }
394 
OnObjectSignaled(HANDLE object)395 void NetworkChangeNotifierWin::OnObjectSignaled(HANDLE object) {
396   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
397   DCHECK(is_watching_);
398   is_watching_ = false;
399 
400   // Start watching for the next address change.
401   WatchForAddressChange();
402 
403   RecomputeCurrentConnectionTypeOnBlockingSequence(base::BindOnce(
404       &NetworkChangeNotifierWin::NotifyObservers, weak_factory_.GetWeakPtr()));
405 }
406 
NotifyObservers(ConnectionType connection_type)407 void NetworkChangeNotifierWin::NotifyObservers(ConnectionType connection_type) {
408   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
409   SetCurrentConnectionType(connection_type);
410   NotifyObserversOfIPAddressChange();
411 
412   // Calling GetConnectionType() at this very moment is likely to give
413   // the wrong result, so we delay that until a little bit later.
414   //
415   // The one second delay chosen here was determined experimentally
416   // by adamk on Windows 7.
417   // If after one second we determine we are still offline, we will
418   // delay again.
419   offline_polls_ = 0;
420   timer_.Start(FROM_HERE, base::Seconds(1), this,
421                &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
422 }
423 
WatchForAddressChange()424 void NetworkChangeNotifierWin::WatchForAddressChange() {
425   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
426   DCHECK(!is_watching_);
427 
428   // NotifyAddrChange occasionally fails with ERROR_OPEN_FAILED for unknown
429   // reasons.  More rarely, it's also been observed failing with
430   // ERROR_NO_SYSTEM_RESOURCES.  When either of these happens, we retry later.
431   if (!WatchForAddressChangeInternal()) {
432     ++sequential_failures_;
433 
434     base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
435         FROM_HERE,
436         base::BindOnce(&NetworkChangeNotifierWin::WatchForAddressChange,
437                        weak_factory_.GetWeakPtr()),
438         base::Milliseconds(kWatchForAddressChangeRetryIntervalMs));
439     return;
440   }
441 
442   // Treat the transition from NotifyAddrChange failing to succeeding as a
443   // network change event, since network changes were not being observed in
444   // that interval.
445   if (sequential_failures_ > 0) {
446     RecomputeCurrentConnectionTypeOnBlockingSequence(
447         base::BindOnce(&NetworkChangeNotifierWin::NotifyObservers,
448                        weak_factory_.GetWeakPtr()));
449   }
450 
451   is_watching_ = true;
452   sequential_failures_ = 0;
453 }
454 
WatchForAddressChangeInternal()455 bool NetworkChangeNotifierWin::WatchForAddressChangeInternal() {
456   DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
457 
458   ResetEventIfSignaled(addr_overlapped_.hEvent);
459   HANDLE handle = nullptr;
460   DWORD ret = NotifyAddrChange(&handle, &addr_overlapped_);
461   if (ret != ERROR_IO_PENDING)
462     return false;
463 
464   addr_watcher_.StartWatchingOnce(addr_overlapped_.hEvent, this);
465   return true;
466 }
467 
NotifyParentOfConnectionTypeChange()468 void NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange() {
469   RecomputeCurrentConnectionTypeOnBlockingSequence(base::BindOnce(
470       &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChangeImpl,
471       weak_factory_.GetWeakPtr()));
472 }
473 
NotifyParentOfConnectionTypeChangeImpl(ConnectionType connection_type)474 void NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChangeImpl(
475     ConnectionType connection_type) {
476   SetCurrentConnectionType(connection_type);
477   bool current_offline = IsOffline();
478   offline_polls_++;
479   // If we continue to appear offline, delay sending out the notification in
480   // case we appear to go online within 20 seconds.  UMA histogram data shows
481   // we may not detect the transition to online state after 1 second but within
482   // 20 seconds we generally do.
483   if (last_announced_offline_ && current_offline && offline_polls_ <= 20) {
484     timer_.Start(FROM_HERE, base::Seconds(1), this,
485                  &NetworkChangeNotifierWin::NotifyParentOfConnectionTypeChange);
486     return;
487   }
488   if (last_announced_offline_)
489     UMA_HISTOGRAM_CUSTOM_COUNTS("NCN.OfflinePolls", offline_polls_, 1, 50, 50);
490   last_announced_offline_ = current_offline;
491 
492   NotifyObserversOfConnectionTypeChange();
493   double max_bandwidth_mbps = 0.0;
494   ConnectionType max_connection_type = CONNECTION_NONE;
495   GetCurrentMaxBandwidthAndConnectionType(&max_bandwidth_mbps,
496                                           &max_connection_type);
497   NotifyObserversOfMaxBandwidthChange(max_bandwidth_mbps, max_connection_type);
498 }
499 
500 }  // namespace net
501