152 lines
7.9 KiB
Rust
152 lines
7.9 KiB
Rust
use irtt_proto::{Params, StampAt, PROTOCOL_VERSION};
|
|
|
|
use crate::{
|
|
clock::saturating_ns,
|
|
config::{ServerConfig, TimestampAllowance},
|
|
};
|
|
|
|
/// Produces the parameters the server returns in an open reply and will enforce
|
|
/// for the session.
|
|
///
|
|
/// **Protocol version is rewritten to [`PROTOCOL_VERSION`].** A server must
|
|
/// return version 1 so that version-1 clients accept the session; any requested
|
|
/// value, including an absent, zero, negative or future one, is accepted and
|
|
/// answered with 1. Detecting a version mismatch is therefore client-side only.
|
|
///
|
|
/// **A positive Length above the configured maximum is reduced to it.** That
|
|
/// bounds the echo datagram — and so the buffer — a remote peer can make this
|
|
/// server negotiate, which is why it happens here rather than at send time: the
|
|
/// value returned in the open reply has to be the one the server will actually
|
|
/// honor. A negative Length is left exactly as requested and a zero stays zero;
|
|
/// neither asks for space beyond the mandatory field block, which `irtt-proto`
|
|
/// floors echo sizing at, so there is nothing to reduce and rewriting them would
|
|
/// answer with a session the client did not ask for.
|
|
///
|
|
/// **Duration is reduced to the configured maximum test duration**, when one is
|
|
/// configured. A *continuous* request — Duration absent, which is the wire
|
|
/// default of zero — is answered with the maximum as well, because restricting
|
|
/// continuous mode to a finite test is exactly what configuring a maximum asks
|
|
/// for; the client models that as an ordinary duration reduction. An explicit
|
|
/// zero or negative Duration never reaches here, being refused during request
|
|
/// admission, and a configured maximum is never zero, so a finite request can
|
|
/// never be rewritten into a continuous session.
|
|
///
|
|
/// **Interval is raised to the configured minimum and then capped at a quarter
|
|
/// of the idle timeout.** The floor bounds how fast a session may ask to be
|
|
/// answered; the cap keeps a well-behaved client from idling itself out by
|
|
/// sending at the very interval it was given. An absent Interval takes the floor
|
|
/// like any other value below it, since absence means the wire default of zero
|
|
/// rather than a refusal — an explicit zero is refused during request admission.
|
|
///
|
|
/// The two can disagree: a configured minimum above a quarter of the idle
|
|
/// timeout ends with the cap winning, and the returned interval below the
|
|
/// configured minimum. That is deliberate, and `rate` is where the consequence
|
|
/// is settled — the session's allowance then replenishes at the *negotiated*
|
|
/// interval, so this server never enforces a cadence slower than the one it just
|
|
/// handed the client.
|
|
///
|
|
/// **ServerFill is settled separately**, by
|
|
/// [`negotiate_server_fill`](crate::fill::negotiate_server_fill), because it
|
|
/// produces a second result this function has nowhere to put: the effective
|
|
/// fill the session will use, which is not always what the descriptor in
|
|
/// `Params` says. The two run back to back on the same open.
|
|
///
|
|
/// **StampAt is reduced to the configured timestamp allowance**, by
|
|
/// [`restrict_stamp_at`]. The Clock is deliberately untouched: the allowance is
|
|
/// a policy about which timestamps this server provides, and the echo layout
|
|
/// carries no timestamp field once the placement is [`StampAt::None`], so there
|
|
/// is no clock to restrict as well. Rewriting it would answer with a session the
|
|
/// client did not ask for.
|
|
///
|
|
/// **DSCP is forced to zero when the server disallows it**, and otherwise
|
|
/// returned exactly as requested — including a value outside `0..=255`, which
|
|
/// stays in the negotiated parameters and is transported unmarked. Zero is a
|
|
/// negotiated value like any other and `Params::encode` simply omits it, so a
|
|
/// restricted reply carries no DSCP tag and the session's stored parameters
|
|
/// carry zero. That one value is what the runtime derives an unmarked reply
|
|
/// from; no session or transport state records the policy separately.
|
|
///
|
|
/// Both restrictions run here, before the core validates the effective session,
|
|
/// which is what makes a timestamp-disallowing server accept an open that
|
|
/// selected timestamps without sending a Clock.
|
|
///
|
|
/// Negotiation is not by itself an acknowledgement: the core validates what this
|
|
/// function returns and silently discards an open whose effective parameters it
|
|
/// could not execute. That is a separate step on purpose — this function decides
|
|
/// what the session *would* be, and the core decides whether that session can
|
|
/// exist. Capping Length here is not enough on its own, because the mandatory
|
|
/// echo field block can exceed the configured maximum by itself.
|
|
pub(crate) fn negotiate_params(mut requested: Params, config: &ServerConfig) -> Params {
|
|
requested.protocol_version = PROTOCOL_VERSION;
|
|
|
|
// A configured maximum wider than `i64` cannot restrict anything: the wire
|
|
// field cannot carry more than `i64::MAX` in the first place. Saturating
|
|
// there is therefore exact, not an approximation — unlike converting the
|
|
// requested length the other way, which would turn a value this platform
|
|
// cannot represent into a plausible buffer size.
|
|
let max_length = i64::try_from(config.max_packet_length()).unwrap_or(i64::MAX);
|
|
if requested.length > max_length {
|
|
requested.length = max_length;
|
|
}
|
|
|
|
if let Some(maximum) = config.max_test_duration() {
|
|
// Never zero — `with_max_test_duration` stores a zero maximum as no
|
|
// maximum — so this cannot turn a finite request into a continuous one.
|
|
let maximum = saturating_ns(maximum);
|
|
if requested.duration_ns == 0 || requested.duration_ns > maximum {
|
|
requested.duration_ns = maximum;
|
|
}
|
|
}
|
|
|
|
// Floor first, cap second. The order is what lets the cap produce a
|
|
// negotiated interval below the configured minimum, which is the case the
|
|
// rate limiter is written to honor rather than contradict.
|
|
let minimum = saturating_ns(config.min_send_interval());
|
|
if requested.interval_ns < minimum {
|
|
requested.interval_ns = minimum;
|
|
}
|
|
// Divided as a `Duration`, before the conversion rather than after it. The
|
|
// two agree for every timeout `i64` nanoseconds can hold, but saturating
|
|
// first would cap an absurdly long timeout's quarter at `i64::MAX / 4` —
|
|
// reducing intervals the configured cap never meant to reach — where
|
|
// dividing first leaves every representable interval alone, which is what a
|
|
// timeout beyond the wire's range should do.
|
|
let idle_cap = saturating_ns(config.idle_timeout() / 4);
|
|
if idle_cap > 0 && requested.interval_ns > idle_cap {
|
|
requested.interval_ns = idle_cap;
|
|
}
|
|
|
|
requested.stamp_at = restrict_stamp_at(requested.stamp_at, config.timestamp_allowance());
|
|
if !config.dscp_allowed() {
|
|
requested.dscp = 0;
|
|
}
|
|
|
|
requested
|
|
}
|
|
|
|
/// The timestamp placement a server offering `allowance` provides for a session
|
|
/// that requested `requested`.
|
|
///
|
|
/// The mapping is the observed one, from the clean specification's Section 11.4.
|
|
/// Only one row substitutes rather than removes: under [`Single`], a request for
|
|
/// both instants is answered with [`StampAt::Midpoint`] — one instant that still
|
|
/// describes both — and not with whichever of Send or Receive an implementation
|
|
/// happened to prefer. Every already-single placement passes through, and
|
|
/// [`None`] removes all of them.
|
|
///
|
|
/// The allowance bounds reported *instants*, not fields: the clock is untouched,
|
|
/// so a negotiated midpoint on `Clock::Both` still reports one field per domain.
|
|
///
|
|
/// [`Single`]: TimestampAllowance::Single
|
|
/// [`None`]: TimestampAllowance::None
|
|
fn restrict_stamp_at(requested: StampAt, allowance: TimestampAllowance) -> StampAt {
|
|
match allowance {
|
|
TimestampAllowance::Dual => requested,
|
|
TimestampAllowance::Single => match requested {
|
|
StampAt::Both => StampAt::Midpoint,
|
|
other => other,
|
|
},
|
|
TimestampAllowance::None => StampAt::None,
|
|
}
|
|
}
|