blob: 286f364dfa832b7bce8860e992b7b725aa900b58 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/ip_protection/ip_protection_token_cache_manager_impl.h"
#include "base/metrics/histogram_functions.h"
#include "base/rand_util.h"
#include "base/strings/strcat.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "services/network/public/mojom/network_context.mojom.h"
namespace network {
namespace {
// Minimum time before actual expiration that a token is considered
// "expired" and removed. The maximum time is given by the
// `IpPrivacyExpirationFuzz` feature param.
constexpr base::TimeDelta kMinimumFuzzInterval = base::Seconds(5);
// Interval between measurements of the token rates.
const base::TimeDelta kTokenRateMeasurementInterval = base::Minutes(5);
} // namespace
IpProtectionTokenCacheManagerImpl::IpProtectionTokenCacheManagerImpl(
mojo::Remote<network::mojom::IpProtectionConfigGetter>* config_getter,
network::mojom::IpProtectionProxyLayer proxy_layer,
bool disable_cache_management_for_testing)
: batch_size_(net::features::kIpPrivacyAuthTokenCacheBatchSize.Get()),
cache_low_water_mark_(
net::features::kIpPrivacyAuthTokenCacheLowWaterMark.Get()),
config_getter_(config_getter),
proxy_layer_(proxy_layer),
disable_cache_management_for_testing_(
disable_cache_management_for_testing) {
last_token_rate_measurement_ = base::TimeTicks::Now();
// Start the timer. The timer is owned by `this` and thus cannot outlive it.
measurement_timer_.Start(
FROM_HERE, kTokenRateMeasurementInterval, this,
&IpProtectionTokenCacheManagerImpl::MeasureTokenRates);
if (!disable_cache_management_for_testing_) {
// Schedule a call to `MaybeRefillCache()`. This will occur soon, since the
// cache is empty.
ScheduleMaybeRefillCache();
}
}
IpProtectionTokenCacheManagerImpl::~IpProtectionTokenCacheManagerImpl() =
default;
bool IpProtectionTokenCacheManagerImpl::IsAuthTokenAvailable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
RemoveExpiredTokens();
return cache_.size() > 0;
}
// If this is a good time to request another batch of tokens, do so. This
// method is idempotent, and can be called at any time.
void IpProtectionTokenCacheManagerImpl::MaybeRefillCache() {
RemoveExpiredTokens();
if (fetching_auth_tokens_ || !config_getter_ ||
disable_cache_management_for_testing_) {
return;
}
if (!try_get_auth_tokens_after_.is_null() &&
base::Time::Now() < try_get_auth_tokens_after_) {
// We must continue to wait before calling `TryGetAuthTokens()` again, so
// there is nothing we can do to refill the cache at this time. The
// `next_maybe_refill_cache_` timer is probably already set, but an extra
// call to `ScheduleMaybeRefillCache()` doesn't hurt.
ScheduleMaybeRefillCache();
return;
}
if (cache_.size() < cache_low_water_mark_) {
fetching_auth_tokens_ = true;
VLOG(2) << "IPPATC::MaybeRefillCache calling TryGetAuthTokens";
config_getter_->get()->TryGetAuthTokens(
batch_size_, proxy_layer_,
base::BindOnce(
&IpProtectionTokenCacheManagerImpl::OnGotAuthTokens,
weak_ptr_factory_.GetWeakPtr(),
/*attempt_start_time_for_metrics=*/base::TimeTicks::Now()));
}
ScheduleMaybeRefillCache();
}
void IpProtectionTokenCacheManagerImpl::InvalidateTryAgainAfterTime() {
try_get_auth_tokens_after_ = base::Time();
ScheduleMaybeRefillCache();
}
// Schedule the next timed call to `MaybeRefillCache()`. This method is
// idempotent, and may be called at any time.
void IpProtectionTokenCacheManagerImpl::ScheduleMaybeRefillCache() {
// If currently getting tokens, the call will be rescheduled when that
// completes. If there's no getter, there's nothing to do.
if (fetching_auth_tokens_ || !config_getter_ ||
disable_cache_management_for_testing_) {
next_maybe_refill_cache_.Stop();
return;
}
base::Time now = base::Time::Now();
base::TimeDelta delay;
if (cache_.size() < cache_low_water_mark_) {
// If the cache is below the low-water mark, call now or (more likely) at
// the requested backoff time.
if (try_get_auth_tokens_after_.is_null()) {
delay = base::TimeDelta();
} else {
delay = try_get_auth_tokens_after_ - now;
}
} else {
// Call when the next token expires.
delay = cache_[0]->expiration - now;
}
if (delay.is_negative()) {
delay = base::TimeDelta();
}
next_maybe_refill_cache_.Start(
FROM_HERE, delay,
base::BindOnce(&IpProtectionTokenCacheManagerImpl::MaybeRefillCache,
weak_ptr_factory_.GetWeakPtr()));
}
void IpProtectionTokenCacheManagerImpl::OnGotAuthTokens(
const base::TimeTicks attempt_start_time_for_metrics,
std::optional<std::vector<network::mojom::BlindSignedAuthTokenPtr>> tokens,
std::optional<base::Time> try_again_after) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
fetching_auth_tokens_ = false;
if (tokens.has_value()) {
VLOG(2) << "IPPATC::OnGotAuthTokens got " << tokens->size() << " tokens";
try_get_auth_tokens_after_ = base::Time();
// Randomize the expiration time of the tokens, applying the same "fuzz" to
// all tokens in the batch.
if (enable_token_expiration_fuzzing_for_testing_) {
base::TimeDelta fuzz_limit =
net::features::kIpPrivacyExpirationFuzz.Get();
base::TimeDelta fuzz =
base::RandTimeDelta(kMinimumFuzzInterval, fuzz_limit);
for (auto& token : *tokens) {
token->expiration -= fuzz;
}
}
cache_.insert(cache_.end(), std::make_move_iterator(tokens->begin()),
std::make_move_iterator(tokens->end()));
std::sort(cache_.begin(), cache_.end(),
[](network::mojom::BlindSignedAuthTokenPtr& a,
network::mojom::BlindSignedAuthTokenPtr& b) {
return a->expiration < b->expiration;
});
base::UmaHistogramMediumTimes(
"NetworkService.IpProtection.TokenBatchGenerationTime",
base::TimeTicks::Now() - attempt_start_time_for_metrics);
} else {
VLOG(2) << "IPPATC::OnGotAuthTokens back off until " << *try_again_after;
try_get_auth_tokens_after_ = *try_again_after;
}
if (on_try_get_auth_tokens_completed_for_testing_) {
std::move(on_try_get_auth_tokens_completed_for_testing_).Run();
}
ScheduleMaybeRefillCache();
}
std::optional<network::mojom::BlindSignedAuthTokenPtr>
IpProtectionTokenCacheManagerImpl::GetAuthToken() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
RemoveExpiredTokens();
base::UmaHistogramBoolean("NetworkService.IpProtection.GetAuthTokenResult",
cache_.size() > 0);
VLOG(2) << "IPPATC::GetAuthToken with " << cache_.size()
<< " tokens available";
std::optional<network::mojom::BlindSignedAuthTokenPtr> result;
if (cache_.size() > 0) {
result = std::move(cache_.front());
cache_.pop_front();
tokens_spent_++;
}
MaybeRefillCache();
return result;
}
void IpProtectionTokenCacheManagerImpl::RemoveExpiredTokens() {
base::Time fresh_after = base::Time::Now();
// Tokens are sorted, so only the first (soonest to expire) is important.
while (cache_.size() > 0 && cache_[0]->expiration <= fresh_after) {
cache_.pop_front();
tokens_expired_++;
}
// Note that all uses of this method also generate a call to
// `MaybeRefillCache()`, so there is no need to do so here.
}
void IpProtectionTokenCacheManagerImpl::MeasureTokenRates() {
auto now = base::TimeTicks::Now();
auto interval = now - last_token_rate_measurement_;
auto interval_ms = interval.InMilliseconds();
auto denominator = base::Hours(1).InMilliseconds();
if (interval_ms != 0) {
last_token_rate_measurement_ = now;
auto spend_rate = tokens_spent_ * denominator / interval_ms;
std::string proxy_layer =
proxy_layer_ == network::mojom::IpProtectionProxyLayer::kProxyA
? "ProxyA"
: "ProxyB";
// A maximum of 1000 would correspond to a spend rate of about 16/min,
// which is higher than we expect to see.
base::UmaHistogramCounts1000(base::StrCat({"NetworkService.IpProtection.",
proxy_layer, ".TokenSpendRate"}),
spend_rate);
auto expiration_rate = tokens_expired_ * denominator / interval_ms;
// Entire batches of tokens are likely to expire within a single 5-minute
// measurement interval. 1024 tokens in 5 minutes is equivalent to 12288
// tokens per hour, comfortably under 100,000.
base::UmaHistogramCounts100000(
base::StrCat({"NetworkService.IpProtection.", proxy_layer,
".TokenExpirationRate"}),
expiration_rate);
}
last_token_rate_measurement_ = now;
tokens_spent_ = 0;
tokens_expired_ = 0;
}
void IpProtectionTokenCacheManagerImpl::DisableCacheManagementForTesting(
base::OnceClosure on_cache_management_disabled) {
disable_cache_management_for_testing_ = true;
ScheduleMaybeRefillCache();
if (fetching_auth_tokens_) {
// If a `TryGetAuthTokens()` call is underway (due to active cache
// management), wait for it to finish.
SetOnTryGetAuthTokensCompletedForTesting( // IN-TEST
std::move(on_cache_management_disabled));
return;
}
std::move(on_cache_management_disabled).Run();
}
void IpProtectionTokenCacheManagerImpl::EnableTokenExpirationFuzzingForTesting(
bool enable) {
enable_token_expiration_fuzzing_for_testing_ = enable;
}
// Call `TryGetAuthTokens()`, which will call
// `on_try_get_auth_tokens_completed_for_testing_` when complete.
void IpProtectionTokenCacheManagerImpl::CallTryGetAuthTokensForTesting() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(config_getter_);
CHECK(on_try_get_auth_tokens_completed_for_testing_);
config_getter_->get()->TryGetAuthTokens(
batch_size_, proxy_layer_,
base::BindOnce(
&IpProtectionTokenCacheManagerImpl::OnGotAuthTokens,
weak_ptr_factory_.GetWeakPtr(),
/*attempt_start_time_for_metrics=*/base::TimeTicks::Now()));
}
} // namespace network