// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/net/network_portal_detector_impl.h"
#include <algorithm>
#include <memory>
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/single_thread_task_runner.h"
#include "build/branding_buildflags.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/net/system_network_context_manager.h"
#include "chromeos/ash/components/dbus/shill/shill_profile_client.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_prefs.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
namespace ash {
namespace {
using ::captive_portal::CaptivePortalDetector;
// Default delay between portal detection attempts when Chrome portal detection
// is used.
constexpr base::TimeDelta kDefaultAttemptDelay = base::Seconds(1);
// Timeout for attempts.
constexpr base::TimeDelta kAttemptTimeout = base::Seconds(10);
const NetworkState* DefaultNetwork() {
return NetworkHandler::Get()->network_state_handler()->DefaultNetwork();
}
// traffic annotation tag.
constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("network_portal_detector", R"(
semantics {
sender: "Network Portal Detector"
description:
"Checks if the system is behind a captive portal. To do so, makes "
"an unlogged, dataless connection to a Google server and checks "
"the response."
trigger:
"Portal detection by the OS is initiated when a new WiFi service "
"is connected to in order to determine whether the network has "
"internet access or is behind a captive portal."
data: "None."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
email: "[email protected]"
}
}
user_data {
type: NONE
}
last_reviewed: "2023-01-13"
}
policy {
cookies_allowed: NO
setting:
"This feature cannot be disabled by settings."
policy_exception_justification:
"This feature is required to deliver core user experiences and "
"cannot be disabled by policy."
})");
void SetNetworkPortalState(const NetworkState* network,
NetworkState::PortalState portal_state) {
NetworkHandler::Get()->network_state_handler()->SetNetworkChromePortalState(
network->path(), portal_state);
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, public:
NetworkPortalDetectorImpl::NetworkPortalDetectorImpl(
network::mojom::URLLoaderFactory* loader_factory_for_testing)
: attempt_timeout_(kAttemptTimeout) {
NET_LOG(EVENT) << "NetworkPortalDetectorImpl::NetworkPortalDetectorImpl()";
network::mojom::URLLoaderFactory* loader_factory;
if (loader_factory_for_testing) {
loader_factory = loader_factory_for_testing;
} else {
shared_url_loader_factory_ =
g_browser_process->system_network_context_manager()
->GetSharedURLLoaderFactory();
loader_factory = shared_url_loader_factory_.get();
}
captive_portal_detector_ =
std::make_unique<CaptivePortalDetector>(loader_factory);
network_state_handler_observer_.Observe(
NetworkHandler::Get()->network_state_handler());
}
NetworkPortalDetectorImpl::~NetworkPortalDetectorImpl() {
NET_LOG(EVENT) << "NetworkPortalDetectorImpl::~NetworkPortalDetectorImpl()";
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
attempt_task_.Cancel();
attempt_timeout_task_.Cancel();
captive_portal_detector_->Cancel();
captive_portal_detector_.reset();
}
bool NetworkPortalDetectorImpl::IsEnabled() {
return enabled_;
}
void NetworkPortalDetectorImpl::Enable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (enabled_)
return;
NET_LOG(EVENT) << "NetworkPortalDetector Enabled.";
DCHECK(is_idle());
enabled_ = true;
const NetworkState* network = DefaultNetwork();
if (!network)
return;
SetNetworkPortalState(network, NetworkState::PortalState::kUnknown);
}
void NetworkPortalDetectorImpl::PortalStateChanged(
const NetworkState* default_network,
NetworkState::PortalState portal_state) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!default_network || !default_network->IsConnectedState()) {
NET_LOG(EVENT) << "No connected default network: "
<< NetworkId(default_network)
<< ", stopping portal detection.";
if ((!default_network && !default_network_id_.empty()) ||
(default_network && default_network->guid() != default_network_id_)) {
default_network_id_ = std::string();
StopDetection();
}
return;
}
default_network_id_ = default_network->guid();
// If a proxy is configured and it is not "direct", then the
// |default_network| has a proxy. By default, managed networks have a
// "direct" proxy. A "direct" proxy is a direct connection to the
// network, so the proxy preferences are ignored.
bool has_proxy = false;
if (default_network->proxy_config().has_value()) {
ProxyConfigDictionary dict(default_network->proxy_config()->Clone());
ProxyPrefs::ProxyMode mode;
if (dict.GetMode(&mode)) {
has_proxy = mode != ProxyPrefs::MODE_DIRECT;
}
}
NET_LOG(EVENT) << "PortalStateChanged, id="
<< NetworkGuidId(default_network_id_)
<< " state=" << default_network->connection_state()
<< " portal_state=" << portal_state
<< " has_proxy=" << has_proxy;
bool schedule_attempt = false;
switch (portal_state) {
case NetworkState::PortalState::kUnknown:
// Not expected. Shill detection failed.
NET_LOG(ERROR) << "Unknown PortalState";
break;
case NetworkState::PortalState::kOnline:
// If a proxy is configured, use captive_portal_detector_ to detect a
// portal.
if (has_proxy) {
schedule_attempt = true;
}
break;
case NetworkState::PortalState::kPortalSuspected:
break;
case NetworkState::PortalState::kPortal:
break;
case NetworkState::PortalState::kNoInternet:
break;
}
if (schedule_attempt) {
ScheduleAttempt();
} else if (!is_idle()) {
StopDetection();
}
}
void NetworkPortalDetectorImpl::OnShuttingDown() {
network_state_handler_observer_.Reset();
}
////////////////////////////////////////////////////////////////////////////////
// NetworkPortalDetectorImpl, private:
void NetworkPortalDetectorImpl::StopDetection() {
if (is_idle()) {
NET_LOG(EVENT) << "StopDetection(): Attempt not running";
return;
}
NET_LOG(EVENT) << "StopDetection";
attempt_task_.Cancel();
attempt_timeout_task_.Cancel();
captive_portal_detector_->Cancel();
state_ = STATE_IDLE;
ResetCountersAndSendMetrics();
}
void NetworkPortalDetectorImpl::ScheduleAttempt(const base::TimeDelta& delay) {
if (!IsEnabled())
return;
if (!is_idle()) {
NET_LOG(EVENT) << "ScheduleAttempt(): Attempt already running, restarting.";
if (state_ == STATE_CHECKING_FOR_PORTAL) {
// When a new attempt is scheduled, cancel any pending attempt to avoid
// a DCHECK in CaptivePortalDetector when an attempt is started before
// the current attempt completes. See b/327072851 for details.
captive_portal_detector_->Cancel();
}
}
attempt_task_.Cancel();
attempt_timeout_task_.Cancel();
state_ = STATE_PORTAL_CHECK_PENDING;
if (attempt_delay_for_testing_) {
next_attempt_delay_ = *attempt_delay_for_testing_;
} else if (!delay.is_zero()) {
next_attempt_delay_ = delay;
} else if (captive_portal_detector_run_count_ == 0) {
// No delay for first attempt.
next_attempt_delay_ = base::TimeDelta();
} else {
next_attempt_delay_ = kDefaultAttemptDelay;
}
attempt_task_.Reset(base::BindOnce(&NetworkPortalDetectorImpl::StartAttempt,
weak_factory_.GetWeakPtr()));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, attempt_task_.callback(), next_attempt_delay_);
}
void NetworkPortalDetectorImpl::StartAttempt() {
DCHECK(is_portal_check_pending());
state_ = STATE_CHECKING_FOR_PORTAL;
const NetworkState* default_network = DefaultNetwork();
if (!default_network) {
NET_LOG(EVENT) << "Start attempt called with no default network, aborting.";
return;
}
GURL url = default_network->probe_url();
if (url.is_empty())
url = GURL(captive_portal::CaptivePortalDetector::kDefaultURL);
NET_LOG(EVENT) << "Starting captive portal detection for: "
<< NetworkId(default_network) << " Probe url: " << url;
captive_portal_detector_->DetectCaptivePortal(
url,
base::BindOnce(&NetworkPortalDetectorImpl::OnAttemptCompleted,
weak_factory_.GetWeakPtr()),
kTrafficAnnotation);
attempt_timeout_task_.Reset(
base::BindOnce(&NetworkPortalDetectorImpl::OnAttemptTimeout,
weak_factory_.GetWeakPtr()));
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, attempt_timeout_task_.callback(), attempt_timeout_);
}
void NetworkPortalDetectorImpl::OnAttemptTimeout() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(is_checking_for_portal());
NET_LOG(EVENT) << "Portal detection timeout: "
<< " id=" << NetworkGuidId(default_network_id_);
captive_portal_detector_->Cancel();
CaptivePortalDetector::Results results;
results.result = captive_portal::RESULT_NO_RESPONSE;
OnAttemptCompleted(results);
}
void NetworkPortalDetectorImpl::OnAttemptCompleted(
const CaptivePortalDetector::Results& results) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(is_checking_for_portal());
captive_portal::CaptivePortalResult result = results.result;
int response_code = results.response_code;
const NetworkState* network = DefaultNetwork();
state_ = STATE_IDLE;
attempt_timeout_task_.Cancel();
bool detection_completed = false;
// |portal_state| defaults to kUnknown which will cause the Chrome portal
// state to be ignored in favor of the Shill portal state.
// See NetworkState::GetPortalState for details.
NetworkState::PortalState portal_state = NetworkState::PortalState::kUnknown;
switch (result) {
case captive_portal::RESULT_NO_RESPONSE:
// Do not override shill results.
if (response_code == net::HTTP_PROXY_AUTHENTICATION_REQUIRED) {
detection_completed = true;
}
break;
case captive_portal::RESULT_INTERNET_CONNECTED:
// Set the portal state to kOnline for metrics reporting. This will not
// override the shill result.
portal_state = NetworkState::PortalState::kOnline;
detection_completed = true;
break;
case captive_portal::RESULT_BEHIND_CAPTIVE_PORTAL:
// Override shill results with kPortal.
// TODO(b/292141089): This should only happen when a proxy is configured
// and Shill is unable to perform accurate portal detection.
portal_state = NetworkState::PortalState::kPortal;
break;
case captive_portal::RESULT_COUNT:
NOTREACHED_IN_MIGRATION();
break;
}
NET_LOG(EVENT) << "NetworkPortalDetector: AttemptCompleted: id="
<< NetworkGuidId(default_network_id_) << ", result="
<< captive_portal::CaptivePortalResultToString(result)
<< ", response_code=" << response_code
<< ", content_length=" << results.content_length.value_or(-1);
captive_portal_detector_run_count_++;
if (detection_completed) {
// Chrome positively identified an online or proxy-auth (407) response.
// No need to continue detection.
response_code_for_testing_ = response_code;
DetectionCompleted(network, portal_state);
return;
}
MaybeReportMetrics(network, portal_state, /*detection_completed=*/false);
if (!is_idle()) {
return;
}
// Set network portal state and continue scheduling attempts until online.
if (portal_state == NetworkState::PortalState::kPortal) {
response_code_for_testing_ = response_code;
SetNetworkPortalState(network, portal_state);
}
ScheduleAttempt(results.retry_after_delta);
}
void NetworkPortalDetectorImpl::MaybeReportMetrics(
const NetworkState* network,
NetworkState::PortalState portal_state,
bool detection_completed) {
if (metrics_reported_) {
return;
}
if (!detection_completed &&
portal_state == NetworkState::PortalState::kUnknown &&
captive_portal_detector_run_count_ < 10) {
return;
}
base::UmaHistogramEnumeration("Network.NetworkPortalDetectorState",
portal_state);
if (network && portal_state == NetworkState::PortalState::kPortal) {
base::UmaHistogramEnumeration("Network.NetworkPortalDetectorType",
network->GetNetworkTechnologyType());
}
metrics_reported_ = true;
}
void NetworkPortalDetectorImpl::DetectionCompleted(
const NetworkState* network,
NetworkState::PortalState portal_state) {
NET_LOG(EVENT) << "NetworkPortalDetector: DetectionCompleted: id="
<< (network ? NetworkGuidId(network->guid()) : "<none>")
<< ", PortalState=" << portal_state;
if (network) {
// Only set kPortal to override the shill result. Setting kUnknown will
// ignore the Chrome result.
if (portal_state == NetworkState::PortalState::kPortal) {
SetNetworkPortalState(network, NetworkState::PortalState::kPortal);
} else {
SetNetworkPortalState(network, NetworkState::PortalState::kUnknown);
}
}
MaybeReportMetrics(network, portal_state, /*detection_completed=*/true);
ResetCountersAndSendMetrics();
}
void NetworkPortalDetectorImpl::ResetCountersAndSendMetrics() {
if (captive_portal_detector_run_count_ > 0) {
base::UmaHistogramCustomCounts("Network.NetworkPortalDetectorRunCount",
captive_portal_detector_run_count_,
/*min=*/1, /*exclusive_max=*/10,
/*buckets=*/10);
captive_portal_detector_run_count_ = 0;
}
metrics_reported_ = false;
}
bool NetworkPortalDetectorImpl::AttemptTimeoutIsCancelledForTesting() const {
return attempt_timeout_task_.IsCancelled();
}
void NetworkPortalDetectorImpl::StartDetectionForTesting() {
ScheduleAttempt();
}
} // namespace ash