// 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
