blob: fc7b65c9542e1596d7c72ed9a765c7975942809f [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Common algorithms.
pub(crate) use port_alloc::*;
/// Ephemeral port allocation provider.
///
/// Defines the [`PortAlloc`] structure and [`PortAllocImpl`] trait, used for
/// ephemeral port allocations in transport protocols.
mod port_alloc {
use alloc::vec::Vec;
use core::hash::Hash;
use core::marker::PhantomData;
use core::num::{NonZeroU16, NonZeroUsize};
use core::ops::RangeInclusive;
use mundane::{hash::Digest, hmac::HmacSha256};
use net_types::{ip::IpAddress, SpecifiedAddr};
use rand::RngCore;
use byteorder::{ByteOrder, NativeEndian};
/// A port number.
// NOTE(brunodalbo): `PortNumber` could be a trait, but given the expected
// use of the PortAlloc algorithm is to allocate `u16` ports, it's just
// defined as a type alias for simplicity.
pub(crate) type PortNumber = u16;
/// A common implementation of `HashableId` providing usual 3-tuple flow
/// identifiers.
///
/// `ProtocolFlowId` provides the most common 3-tuple needed
/// to be used with a [`PortAlloc`] structure: local IP,
/// remote IP, and remote port number.
#[derive(Hash, Debug)]
pub(crate) struct ProtocolFlowId<I: IpAddress> {
local_addr: SpecifiedAddr<I>,
remote_addr: SpecifiedAddr<I>,
remote_port: NonZeroU16,
}
impl<I: IpAddress> ProtocolFlowId<I> {
/// Creates a new `ProtocolFlowId` with given parameters.
pub(crate) fn new(
local_addr: SpecifiedAddr<I>,
remote_addr: SpecifiedAddr<I>,
remote_port: NonZeroU16,
) -> Self {
Self { local_addr, remote_addr, remote_port }
}
/// Gets this `ProtocolFlowId`'s local address.
pub(crate) fn local_addr(&self) -> &SpecifiedAddr<I> {
&self.local_addr
}
/// Gets this `ProtocolFlowId`'s remote address.
pub(crate) fn remote_addr(&self) -> &SpecifiedAddr<I> {
&self.remote_addr
}
/// Gets this `ProtocolFlowId`'s remote port number.
pub(crate) fn remote_port(&self) -> NonZeroU16 {
self.remote_port
}
}
/// Length, in bytes, of secret numbers used for port allocations.
const SECRET_LEN: usize = 16;
/// Trait that configures the behavior of a [`PortAlloc`] structure.
///
/// `PortAllocImpl` provides the types, custom behaviors, and port
/// availability checks necessary to operate the port allocation algorithm
/// provided by [`PortAlloc`].
pub(crate) trait PortAllocImpl {
/// The number of different incremental ranges used by [`PortAlloc`].
/// If `TABLE_SIZE` is 1, the algorithm used by [`PortAlloc`] is
/// Algorithm 3, as described in [RFC 6056]. Otherwise, [`PortAlloc`]
/// will use Algorithm 4.
///
/// [RFC 6056]: https://tools.ietf.org/html/rfc6056
const TABLE_SIZE: NonZeroUsize;
/// The range of ports that can be allocated by [`PortAlloc`].
///
/// Local ports used in transport protocols are called [Ephemeral
/// Ports]. Different transport protocols may define different ranges
/// for the issued ports. [`PortAlloc`] is guaranteed to return a port
/// in this range as a result of [`PortAlloc::try_alloc`].
///
/// [Ephemeral Ports]: https://tools.ietf.org/html/rfc6056#section-2
const EPHEMERAL_RANGE: RangeInclusive<PortNumber>;
/// The "flow" identifier used to allocate port Ids.
///
/// The `Id` is typically the 3 elements other other than the local port
/// in the 4-tuple (local IP:port, remote IP:port) that is used to
/// uniquely identify the flow information of a connection.
type Id: Hash;
/// Returns the number of ephemeral ports avaiable in `EPHEMERAL_RANGE`.
fn num_ephemeral() -> usize {
usize::from(Self::EPHEMERAL_RANGE.end() - Self::EPHEMERAL_RANGE.start()) + 1
}
/// Returns a random ephemeral port in `EPHEMERAL_RANGE`
fn rand_ephemeral<R: RngCore>(rng: &mut R) -> EphemeralPort<Self> {
EphemeralPort::new_random(rng)
}
/// Checks if `port` is available to be used for the flow `id`.
///
/// Implementers return `true` if the provided `port` is available to be
/// used for a given flow `id`. An available port is a port that would
/// not conflict for the given `id` *plus* ideally the port is not in
/// LISTEN or CLOSED states for a given protocol (see [RFC 6056]).
///
/// [RFC 6056]: https://tools.ietf.org/html/rfc6056#section-2.2
fn is_port_available(&self, id: &Self::Id, port: PortNumber) -> bool;
}
/// Provides a port allocation algorithm, following the guidelines in [RFC
/// 6056].
///
/// `PortAlloc` provides the port allocation algorithms 3 or 4 (depending on
/// the [`PortAllocImpl`] type parameter) described in [`RFC 6056`].
///
/// The algorithm consists of 2 sources of obfuscation:
/// - For a given flow identifier, a base offset is calculated from an HMAC.
/// - For a given flow identifier, a *different* hash provides an index into
/// an internal table that keeps monotonically increasing port numbers.
///
/// The tentative port is acquired by selecting a port in the provided
/// `EPHEMERAL_RANGE` using the two obfuscated offsets described above, and
/// then a check for conflicts is performed. A simple linear scan from the
/// initial calculated port is performed from that point, until an available
/// port is found.
///
/// `PortAlloc` holds two secrets internally, that are used to generate the
/// 2 sources of obfuscation above. The secrets can be updated periodically
/// using [`PortAlloc::update_secrets`].
///
/// [RFC 6056]: https://tools.ietf.org/html/rfc6056
pub(crate) struct PortAlloc<I: PortAllocImpl> {
// TODO(brunodalbo): table can be made an array once we can declare
// arrays with associated consts
table: Vec<PortNumber>,
secret_a: [u8; SECRET_LEN],
secret_b: [u8; SECRET_LEN],
_marker: core::marker::PhantomData<I>,
}
/// A witness type for a port within some ephemeral port range.
///
/// `EphemeralPort` is always guaranteed to contain a port that is within
/// `I::EPHEMERAL_RANGE`.
pub(crate) struct EphemeralPort<I: PortAllocImpl + ?Sized> {
port: PortNumber,
_marker: PhantomData<I>,
}
impl<I: PortAllocImpl + ?Sized> EphemeralPort<I> {
/// Creates a new `EphemeralPort` with a port chosen randomly in
/// `range`.
pub(crate) fn new_random<R: RngCore>(rng: &mut R) -> Self {
let num_ephemeral =
u32::from(I::EPHEMERAL_RANGE.end() - I::EPHEMERAL_RANGE.start()) + 1;
let port =
I::EPHEMERAL_RANGE.start() + ((rng.next_u32() % num_ephemeral) as PortNumber);
Self { port, _marker: PhantomData }
}
/// Increments the current [`PortNumber`] to the next value in the
/// contained range, wrapping around to the start of the range.
pub(crate) fn next(&mut self) {
if self.port == *I::EPHEMERAL_RANGE.end() {
self.port = *I::EPHEMERAL_RANGE.start();
} else {
self.port += 1;
}
}
/// Gets the `PortNumber` value.
pub(crate) fn get(&self) -> PortNumber {
self.port
}
}
impl<I: PortAllocImpl> PortAlloc<I> {
/// Creates a new `PortAlloc` port allocation provider.
///
/// `rng` is used to generate the initial secrets and random offsets in
/// the internal table.
// TODO(brunodalbo) make R: RngCore + CryptoRng when we tighten the
// security around this algorithm.
pub(crate) fn new<R: RngCore>(rng: &mut R) -> Self {
let mut table = Vec::with_capacity(I::TABLE_SIZE.into());
for _ in 0..I::TABLE_SIZE.into() {
table.push(rng.next_u32() as PortNumber);
}
let mut secret_a = [0; SECRET_LEN];
let mut secret_b = [0; SECRET_LEN];
rng.fill_bytes(&mut secret_a[..]);
rng.fill_bytes(&mut secret_b[..]);
Self { table, secret_a, secret_b, _marker: core::marker::PhantomData }
}
/// Attempts to allocate a new port for a flow with the given `id`.
///
/// `try_alloc` performs the algorithm 3 or 4, as described in [RFC
/// 6056] and returns `Some(PortNumber)` if an available port could be
/// selected (`state` provides the availability checks) or `None`
/// otherwise.
///
/// [RFC 6056]: https://tools.ietf.org/html/rfc6056
pub(crate) fn try_alloc(&mut self, id: &I::Id, state: &I) -> Option<PortNumber> {
let num_ephemeral = I::num_ephemeral();
let offset = hmac_with_secret(id, &self.secret_a[..]);
let table_index = hmac_with_secret(id, &self.secret_b[..]) % self.table.len();
let table_val = &mut self.table[table_index];
for _ in 0..num_ephemeral {
let local_off = offset.wrapping_add(*table_val as usize) % num_ephemeral;
// we can safely cast `local_off` to `PortNumber` because of the
// % num_ephemeral operation above.
let port = I::EPHEMERAL_RANGE.start() + (local_off as PortNumber);
*table_val = table_val.wrapping_add(1);
if state.is_port_available(id, port) {
return Some(port);
}
}
None
}
/// Updates the internal secrets used in the Hash-Based port selection.
///
/// It is interesting to update the internal secrets periodically,
/// making a remote attack involving port number predictions harder.
/// [RFC 6056 section 3.4] suggest this can be done periodically. Care
/// must be taken in doing so, because the new offsets calculated from
/// refreshed secrets can cause collisions of instance-ids.
///
/// [RFC 6056 section 3.4]: https://tools.ietf.org/html/rfc6056#section-3.4
// TODO(brunodalbo) make R: RngCore + CryptoRng when we tighten the
// security around this algorithm.
// TODO(rheacock): Remove _ prefix when this function is used.
pub(crate) fn _update_secrets<R: RngCore>(&mut self, rng: &mut R) {
rng.fill_bytes(&mut self.secret_a[..]);
rng.fill_bytes(&mut self.secret_b[..]);
}
}
/// Helper function to hash an `id` with a given `secret` into a `usize`.
fn hmac_with_secret<I: Hash>(id: &I, secret: &[u8]) -> usize {
let mut hmac = HmacSha256::new(secret);
id.hash(&mut hmac);
NativeEndian::read_u64(&hmac.finish().bytes()) as usize
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutil::{with_fake_rngs, FakeCryptoRng};
/// A mock flow identifier.
#[derive(Hash)]
struct MockId(usize);
/// Number of different RNG seeds used in tests in this mod.
const RNG_ROUNDS: u64 = 128;
/// Hard-coded mock of available port filter.
enum MockAvailable {
/// Only a single port is available.
AllowSingle(PortNumber),
/// No ports are available.
DenyAll,
/// Only even-numbered ports are available.
AllowEvens,
/// All ports are available.
AllowAll,
}
/// Mock implementation of [`PortAllocImpl`].
///
/// The `available` field will dictate the return of
/// [`PortAllocImpl::is_port_available`] and can be set to get the
/// expected testing behavior.
struct MockImpl {
available: MockAvailable,
}
impl PortAllocImpl for MockImpl {
const TABLE_SIZE: NonZeroUsize = unsafe { NonZeroUsize::new_unchecked(2) };
const EPHEMERAL_RANGE: RangeInclusive<u16> = 100..=200;
type Id = MockId;
fn is_port_available(&self, _id: &Self::Id, port: u16) -> bool {
match self.available {
MockAvailable::AllowEvens => (port & 1) == 0,
MockAvailable::DenyAll => false,
MockAvailable::AllowSingle(p) => port == p,
MockAvailable::AllowAll => true,
}
}
}
/// Helper fn to test that if only a single port is available, we will
/// eventually get that
fn test_allow_single(single: u16) {
with_fake_rngs(RNG_ROUNDS, |mut rng| {
let mock = MockImpl { available: MockAvailable::AllowSingle(single) };
let mut alloc = PortAlloc::<MockImpl>::new(&mut rng);
let port = alloc.try_alloc(&MockId(0), &mock);
assert_eq!(port.unwrap(), single);
});
}
#[test]
fn test_single_range_start() {
// test boundary condition for first ephemeral port:
test_allow_single(MockImpl::EPHEMERAL_RANGE.start().clone())
}
#[test]
fn test_single_range_end() {
// test boundary condition for last ephemeral port:
test_allow_single(MockImpl::EPHEMERAL_RANGE.end().clone())
}
#[test]
fn test_single_range_mid() {
// test some other ephemeral port:
test_allow_single(
(MockImpl::EPHEMERAL_RANGE.end() + MockImpl::EPHEMERAL_RANGE.start()) / 2,
)
}
#[test]
fn test_allow_none() {
// test that if no ports are available, try_alloc must return none:
with_fake_rngs(RNG_ROUNDS, |mut rng| {
let mock = MockImpl { available: MockAvailable::DenyAll };
let mut alloc = PortAlloc::<MockImpl>::new(&mut rng);
let port = alloc.try_alloc(&MockId(0), &mock);
assert!(port.is_none());
});
}
#[test]
fn test_allow_evens() {
// test that if we only allow even ports, we will always get ports
// in the specified range, and they'll always be even:
with_fake_rngs(RNG_ROUNDS, |mut rng| {
let mock = MockImpl { available: MockAvailable::AllowEvens };
let mut alloc = PortAlloc::<MockImpl>::new(&mut rng);
let port = alloc.try_alloc(&MockId(0), &mock).unwrap();
assert!(MockImpl::EPHEMERAL_RANGE.contains(&port));
assert_eq!(port & 1, 0);
});
}
#[test]
fn test_sequential_allocs() {
// test that for a single flow Id, we get sequential ports that can
// span the entire ephemeral range:
with_fake_rngs(RNG_ROUNDS, |mut rng| {
let mock = MockImpl { available: MockAvailable::AllowAll };
let mut alloc = PortAlloc::<MockImpl>::new(&mut rng);
let mut port = alloc.try_alloc(&MockId(0), &mock).unwrap();
assert!(MockImpl::EPHEMERAL_RANGE.contains(&port));
for _ in MockImpl::EPHEMERAL_RANGE {
let next = alloc.try_alloc(&MockId(0), &mock).unwrap();
let expect = if port == *MockImpl::EPHEMERAL_RANGE.end() {
*MockImpl::EPHEMERAL_RANGE.start()
} else {
port + 1
};
assert_eq!(next, expect);
port = next;
}
});
}
#[test]
fn test_different_tables() {
// test that different Ids can hash to different offsets in internal
// tables, which increase independently.
let mock = MockImpl { available: MockAvailable::AllowAll };
let mut alloc = PortAlloc::<MockImpl>::new(&mut FakeCryptoRng::new_xorshift(0));
let table_a = alloc.table[0];
let table_b = alloc.table[1];
let id_a = MockId(0);
let id_b = MockId(1);
assert_eq!(hmac_with_secret(&id_a, &alloc.secret_b[..]) % 2, 0);
assert_eq!(hmac_with_secret(&id_b, &alloc.secret_b[..]) % 2, 1);
let _ = alloc.try_alloc(&id_a, &mock).unwrap();
// a single allocation should've moved offset a but not b:
assert_eq!(table_a.wrapping_add(1), alloc.table[0]);
assert_eq!(table_b, alloc.table[1]);
let _ = alloc.try_alloc(&id_b, &mock).unwrap();
// now offset b should've moved, and a remained just one forward:
assert_eq!(table_a.wrapping_add(1), alloc.table[0]);
assert_eq!(table_b.wrapping_add(1), alloc.table[1]);
}
#[test]
fn test_ephemeral_port_random() {
// test that random ephemeral ports are always in range
let mut rng = FakeCryptoRng::new_xorshift(0);
for _ in 0..1000 {
let rnd_port = EphemeralPort::<MockImpl>::new_random(&mut rng);
assert!(MockImpl::EPHEMERAL_RANGE.contains(&rnd_port.port));
}
}
#[test]
fn test_ephemeral_port_next() {
let mut port = EphemeralPort::<MockImpl> {
port: *MockImpl::EPHEMERAL_RANGE.start(),
_marker: PhantomData,
};
// loop over all the range twice so we see the wrap-around
for _ in 0..=1 {
for x in MockImpl::EPHEMERAL_RANGE {
assert_eq!(port.port, x);
port.next();
}
}
}
}
}