Files
irtt-rs/crates/irtt-server/src/fill.rs
T
Nils Andreas Svee 48a50d7f3a
Rust CI / cargo fmt (push) Successful in 17s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 34s
Rust CI / cargo clippy (push) Successful in 35s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 10s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 23s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 9s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 3s
Netem Interop / netem interop (push) Successful in 1m11s
tests: rework the actually useful tests to live in the integration crate
2026-10-03 13:59:38 +07:00

218 lines
9.8 KiB
Rust

//! Server fill policy: what the ServerFill descriptor means, and what bytes it
//! puts in an echo reply's payload region.
//!
//! Descriptor *strings* are wire data and `irtt-proto` carries them verbatim;
//! deciding what one means is server policy and lives here. The result is a
//! [`FillMode`], which is deliberately crate-private: it is neither wire format
//! nor public configuration.
use irtt_proto::{echo_packet_len, PacketLayout, Params, ServerFill};
/// The bytes of this server's default fill, repeated: `69 72 74 74`, or `irtt`.
///
/// The value matches the descriptor the clean evidence records the reference
/// server defaulting to, so a client that recognizes it sees what it expects.
/// Its use here is nevertheless `irtt-rs` policy: payload bytes carry no
/// protocol meaning, and nothing about interoperability requires this pattern
/// over any other.
const DEFAULT_FILL_PATTERN: &[u8] = b"irtt";
/// The descriptor naming [`DEFAULT_FILL_PATTERN`], as returned in an open reply
/// whose explicit request this server could not honor.
///
/// Held as a constant beside the bytes rather than derived from them, so the
/// descriptor a client is told and the bytes it is sent cannot drift apart
/// through a formatting or parsing step neither of them needs.
pub(crate) const DEFAULT_FILL_DESCRIPTOR: &str = "pattern:69727474";
/// The mode prefix of a repeating hexadecimal pattern descriptor.
const PATTERN_PREFIX: &str = "pattern:";
/// How this server fills the payload region of one session's echo replies.
///
/// Crate-private on purpose. It is an interpretation of a wire string, not the
/// string itself and not a configuration surface, so nothing outside the crate
/// has any use for it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum FillMode {
/// Write no fill of the server's own. The payload region is zeroes — see
/// [`FillMode::payload`].
None,
/// Fill the payload region with bytes from the operating system's random
/// source.
Random,
/// Fill the payload region with these bytes, repeated. Never empty.
Pattern(Vec<u8>),
}
impl FillMode {
/// This server's default fill, used by a session whose client expressed no
/// preference and by one whose explicit descriptor could not be honored.
pub(crate) fn default_fill() -> Self {
Self::Pattern(DEFAULT_FILL_PATTERN.to_vec())
}
/// The mode a descriptor asks for, or `None` if it names none.
///
/// The three mode names are matched exactly and case-sensitively; only a
/// pattern's hexadecimal body is case-insensitive. An unrecognized name, a
/// differently-cased one, and a `pattern:` whose body is empty, odd-length
/// or not hexadecimal all return `None`, and negotiation replaces them with
/// the default. Nothing here rejects an open, panics, or half-parses a
/// descriptor into a partial pattern.
fn parse(descriptor: &str) -> Option<Self> {
match descriptor {
"none" => Some(Self::None),
"rand" => Some(Self::Random),
_ => descriptor
.strip_prefix(PATTERN_PREFIX)
.and_then(parse_hex_pattern)
.map(Self::Pattern),
}
}
/// The `len` payload bytes an echo reply carries under this mode.
///
/// **A pattern always starts at its first byte.** The phase resets for every
/// reply, which is `irtt-rs` policy: the clean evidence records the
/// reference implementation advancing one phase continuously across replies,
/// sessions and listeners, and states equally clearly that payload phase has
/// no protocol significance and that a conforming client must not depend on
/// it. Resetting is therefore interoperability-equivalent, and it buys
/// determinism — no global mutable fill state, no coupling between sessions
/// or listeners, and a reply whose bytes a test can state exactly.
///
/// Infallible by construction. A pattern and an empty region cannot fail,
/// and a random draw that does falls back to zeroes rather than failing the
/// reply.
pub(crate) fn payload(&self, len: usize) -> Vec<u8> {
match self {
// No allocation and no copy: the encoder already zero-fills the
// whole region between the field block and the negotiated length,
// so an empty payload *is* the zero payload. Zero is the policy —
// the clean evidence records the reference server leaving this
// region as unspecified residual buffer content, which returned
// other traffic's bytes, and a compatible server must not.
Self::None => Vec::new(),
Self::Random => random_payload(len),
Self::Pattern(pattern) => pattern.iter().copied().cycle().take(len).collect(),
}
}
}
/// The pattern a `pattern:` descriptor's body names, or `None` if the body is
/// not a whole number of hexadecimal bytes.
///
/// A descriptor is at most [`MAX_SERVER_FILL_BYTES`] on the wire, so this
/// decodes at most twelve bytes and complexity is beside the point. Both digit
/// cases are accepted; nothing else is, including a non-ASCII character, whose
/// bytes are all rejected as digits.
///
/// [`MAX_SERVER_FILL_BYTES`]: irtt_proto::MAX_SERVER_FILL_BYTES
fn parse_hex_pattern(body: &str) -> Option<Vec<u8>> {
let digits = body.as_bytes();
if digits.is_empty() || !digits.len().is_multiple_of(2) {
return None;
}
digits
.as_chunks::<2>()
.0
.iter()
.map(|pair| Some(hex_digit(pair[0])? << 4 | hex_digit(pair[1])?))
.collect()
}
/// The value of one ASCII hexadecimal digit.
fn hex_digit(digit: u8) -> Option<u8> {
match digit {
b'0'..=b'9' => Some(digit - b'0'),
b'a'..=b'f' => Some(digit - b'a' + 10),
b'A'..=b'F' => Some(digit - b'A' + 10),
_ => None,
}
}
/// `len` random bytes from the operating system, or zeroes if the draw failed.
///
/// Failure is not fatal: payload variation has no protocol meaning, so a
/// random-source failure must not terminate a listener or drop a session.
///
/// The bytes exist to vary the payload, and nothing here is a security claim.
/// `getrandom` is used because the crate already depends on it; session tokens,
/// which *are* security state, keep their own separate source.
fn random_payload(len: usize) -> Vec<u8> {
let mut payload = vec![0; len];
if len > 0 && getrandom::fill(&mut payload).is_err() {
payload.fill(0);
}
payload
}
/// Settles what fill a session will actually use, restricting `params` only
/// where this server could not honor what was asked for.
///
/// Four cases, and the distinction between the first two and the last is the
/// point:
///
/// - **Absent or empty descriptor.** The client expressed no preference, so
/// `params` is left exactly as it arrived — absent stays absent, an explicit
/// empty value stays empty — and the session uses this server's default fill.
/// Writing the default descriptor into the reply would manufacture a
/// restriction out of a request that made none, and a strict client rejects a
/// descriptor it never asked for.
/// - **A recognized, well-formed descriptor.** Honored exactly, and returned
/// byte-for-byte, including a pattern body whose hexadecimal case differs from
/// the decoded bytes. `irtt-rs` accepts every valid `none`, `rand` and
/// `pattern:` descriptor: this is deliberately more permissive than the
/// reference server's default glob allow-list, which is upstream policy rather
/// than an interoperability requirement, and refusing valid modes would create
/// strict-negotiation failures while preventing nothing — the payload carries
/// no protocol meaning, the descriptor is bounded to 32 wire bytes, a pattern
/// is tiny, `none` is safely zero-filled here, and a random fill is bounded by
/// the negotiated packet length like every other reply.
/// - **An unrecognized or malformed explicit descriptor.** The server really did
/// change what was asked for, so it says so: `params.server_fill` becomes
/// [`DEFAULT_FILL_DESCRIPTOR`] and the session uses the default fill. A strict
/// client is then free to reject the session — the restriction is honest, and
/// hiding it would be worse.
///
/// The descriptor is parsed exactly once, here, so echo processing never looks
/// at a string.
pub(crate) fn negotiate_server_fill(params: &mut Params) -> FillMode {
let Some(requested) = params
.server_fill
.as_ref()
.filter(|fill| !fill.value.is_empty())
else {
return FillMode::default_fill();
};
match FillMode::parse(&requested.value) {
Some(mode) => mode,
None => {
params.server_fill = Some(ServerFill {
value: DEFAULT_FILL_DESCRIPTOR.to_owned(),
});
FillMode::default_fill()
}
}
}
/// The payload region of one echo reply under `params`, in bytes.
///
/// Derived from `irtt-proto`'s own layout and sizing rather than from
/// `params.length`, which is neither the packet length nor the payload length:
/// the mandatory field block, the negotiated statistics and timestamps, and
/// authentication's 16 bytes all consume packet space, and a length below the
/// field block asks for no payload at all.
///
/// An unrepresentable length yields zero rather than an error. An acknowledged
/// session cannot reach it — the open path already required [`echo_packet_len`]
/// to succeed — and if one somehow did, encoding the reply would fail on that
/// same call, which is where the failure belongs.
///
/// [`echo_packet_len`]: irtt_proto::echo_packet_len
pub(crate) fn echo_payload_len(hmac: bool, params: &Params) -> usize {
let header_len = PacketLayout::echo(hmac, params).header_len();
echo_packet_len(hmac, params).map_or(0, |len| len.saturating_sub(header_len))
}