blob: bc8db4600986994b2b8fd66569f31397282b7f28 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/socket/ephemeral_port_randomizer_mac.h"
#include <sys/sysctl.h>
#include <memory>
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "net/base/features.h"
namespace net {
namespace {
constexpr uint16_t kDefaultEphemeralPortStart = 49152;
constexpr uint16_t kDefaultEphemeralPortEnd = 65535;
bool ReadSysctlInt(const char* name, int* value) {
size_t value_len = sizeof(*value);
if (sysctlbyname(name, value, &value_len, nullptr, 0) != 0) {
return false;
}
return value_len == sizeof(*value);
}
uint16_t DefaultRandInt(uint16_t first, uint16_t last) {
return static_cast<uint16_t>(base::RandIntInclusive(first, last));
}
} // namespace
EphemeralPortRandomizer::EphemeralPortRandomizer()
: EphemeralPortRandomizer(
ReadPortRangeFromSysctl(),
base::Seconds(features::kTcpPortRandomizationReuseDelaySec.Get()),
base::BindRepeating(&DefaultRandInt)) {}
EphemeralPortRandomizer::EphemeralPortRandomizer(
PortRange port_range,
base::TimeDelta reuse_delay,
RandIntCallback rand_int_callback)
: port_range_(port_range),
reuse_delay_(reuse_delay),
rand_int_callback_(std::move(rand_int_callback)) {
CHECK(rand_int_callback_);
DETACH_FROM_SEQUENCE(sequence_checker_);
}
// static
std::unique_ptr<EphemeralPortRandomizer>
EphemeralPortRandomizer::CreateForTesting(PortRange port_range,
base::TimeDelta reuse_delay,
RandIntCallback rand_int_callback) {
return base::WrapUnique(new EphemeralPortRandomizer(
port_range, reuse_delay, std::move(rand_int_callback)));
}
EphemeralPortRandomizer::~EphemeralPortRandomizer() = default;
std::optional<uint16_t> EphemeralPortRandomizer::PickPort(
const IPEndPoint& peer_address) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const PortRange& range = port_range_;
if (range.first == 0 || range.last == 0 || range.first > range.last) {
return std::nullopt;
}
uint32_t range_size = static_cast<uint32_t>(range.last - range.first) + 1;
uint16_t start_port = rand_int_callback_.Run(range.first, range.last);
base::TimeTicks now = base::TimeTicks::Now();
// Look up the peer entry once. If there are no recently-used ports for this
// peer, we can return immediately without looping. Record the port as used
// immediately so subsequent callers won't pick the same port.
auto [peer_it, inserted] = recent_ports_by_peer_.try_emplace(
peer_address, PortLRUCache::NO_AUTO_EVICT);
if (inserted) {
peer_it->second.Put(start_port, now);
return start_port;
}
auto& port_cache = peer_it->second;
CleanupExpired(port_cache, now);
if (port_cache.empty()) {
port_cache.Put(start_port, now);
return start_port;
}
for (uint32_t attempt = 0; attempt < range_size; ++attempt) {
uint32_t offset =
(static_cast<uint32_t>(start_port - range.first) + attempt) %
range_size;
uint16_t candidate = static_cast<uint16_t>(range.first + offset);
// Use Peek() to avoid reordering the LRU cache during lookup.
auto port_it = port_cache.Peek(candidate);
if (port_it == port_cache.end() ||
(now - port_it->second) >= reuse_delay_) {
port_cache.Put(candidate, now);
return candidate;
}
}
base::UmaHistogramBoolean("Net.EphemeralPortRandomizer.PickPortExhausted",
true);
return std::nullopt;
}
void EphemeralPortRandomizer::RecordPortUse(const IPEndPoint& peer_address,
uint16_t port) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks now = base::TimeTicks::Now();
auto [peer_it, inserted] = recent_ports_by_peer_.try_emplace(
peer_address, PortLRUCache::NO_AUTO_EVICT);
auto& port_cache = peer_it->second;
if (!inserted) {
CleanupExpired(port_cache, now);
}
port_cache.Put(port, now);
MaybeStartCleanupTimer();
}
base::DictValue EphemeralPortRandomizer::CreateNetLogTCPBindAttemptStartParams(
const IPEndPoint& peer_address,
int attempt_number) {
const auto& cache_entry = recent_ports_by_peer_.find(peer_address);
return base::DictValue()
.Set("remote_endpoint", peer_address.ToString())
.Set("attempt", attempt_number)
.Set("randomizer_range_first", port_range_.first)
.Set("randomizer_range_last", port_range_.last)
.Set("randomizer_recent_ports_by_remote_endpoint_size",
cache_entry == recent_ports_by_peer_.end()
? 0
: static_cast<int>(cache_entry->second.size()));
}
// static
EphemeralPortRandomizer::PortRange
EphemeralPortRandomizer::ReadPortRangeFromSysctl() {
int first = 0;
int last = 0;
if (!ReadSysctlInt("net.inet.ip.portrange.first", &first) ||
!ReadSysctlInt("net.inet.ip.portrange.last", &last) || first <= 0 ||
last <= 0 || first > last || last > 65535) {
return {kDefaultEphemeralPortStart, kDefaultEphemeralPortEnd};
}
return {static_cast<uint16_t>(first), static_cast<uint16_t>(last)};
}
void EphemeralPortRandomizer::CleanupExpired(PortLRUCache& port_cache,
base::TimeTicks now) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Iterate from the back (least recently used / oldest) and erase expired
// entries. Stop at the first non-expired entry since older entries are
// further back.
while (!port_cache.empty()) {
auto oldest = port_cache.rbegin();
if ((now - oldest->second) >= reuse_delay_) {
port_cache.Erase(oldest);
} else {
break;
}
}
}
void EphemeralPortRandomizer::MaybeStartCleanupTimer() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!cleanup_timer_.IsRunning() && !recent_ports_by_peer_.empty()) {
// This use of base::Unretained is safe because `cleanup_timer_` is owned
// by `this`.
cleanup_timer_.Start(
FROM_HERE, reuse_delay_,
base::BindOnce(&EphemeralPortRandomizer::OnCleanupTimer,
base::Unretained(this)));
}
}
void EphemeralPortRandomizer::OnCleanupTimer() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::TimeTicks now = base::TimeTicks::Now();
absl::erase_if(recent_ports_by_peer_, [&](auto& entry) {
CleanupExpired(entry.second, now);
return entry.second.empty();
});
// Restart the timer if there are still entries to clean up later.
MaybeStartCleanupTimer();
}
// static
EphemeralPortRandomizer& EphemeralPortRandomizer::GetInstance() {
static base::NoDestructor<EphemeralPortRandomizer> instance;
return *instance;
}
} // namespace net