feat(server): add negotiation policy controls

This commit is contained in:
Nils Andreas Svee committed 2026-08-14 14:24:57 +07:00
1 parent 58c04e8d04
commit 8308c077eb
10 files changed
+831 -30

No files matched your search

+108 -3
View File
@@ -53,12 +53,36 @@ pub const DEFAULT_BURST_ALLOWANCE: u32 = 5;
/// emits no final reply. See [`ServerConfig::with_idle_timeout`].
pub const DEFAULT_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
/// How many timestamps a server is willing to provide.
///
/// This is **server policy**, deliberately a separate type from the wire
/// [`StampAt`](irtt_proto::StampAt) a session requests and negotiates: one
/// describes what a client asked for, the other what this server will hand out.
/// Negotiation reduces the requested `StampAt` accordingly, and only it — the
/// requested [`Clock`](irtt_proto::Clock) is never rewritten, because the clean
/// evidence records a timestamp allowance restricting timestamp *placement*, not
/// the clock domains it is read from.
///
/// See [`ServerConfig::with_timestamp_allowance`] for the mapping.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TimestampAllowance {
/// No timestamps at all: every request is negotiated to
/// [`StampAt::None`](irtt_proto::StampAt::None).
None,
/// At most one timestamp field. A request for both receive and send becomes
/// [`StampAt::Midpoint`](irtt_proto::StampAt::Midpoint); every mode that is
/// already single is left alone.
Single,
/// Every requested placement is honored. This is the default, and the
/// behavior of a server that configures nothing.
#[default]
Dual,
}
/// Configuration for a [`ServerCore`](crate::ServerCore).
///
/// Fields are private and set through consuming builder methods, so later
/// slices can add configuration without breaking construction. Optional
/// timestamp-allowance and DSCP restriction controls would arrive with the
/// slices that implement them.
/// slices can add configuration without breaking construction.
///
/// **Server fill is deliberately not configurable.** The policy is fixed and
/// safe: every valid descriptor is honored, one that cannot be parsed falls back
@@ -78,6 +102,8 @@ pub struct ServerConfig {
burst_allowance: u32,
idle_timeout: Duration,
max_test_duration: Option<Duration>,
timestamp_allowance: TimestampAllowance,
dscp_allowed: bool,
}
impl ServerConfig {
@@ -221,6 +247,65 @@ impl ServerConfig {
self
}
/// Sets how many timestamps this server is willing to provide.
///
/// A requested placement above the allowance is reduced during negotiation,
/// so the open reply is honest about what the session's echo replies will
/// carry:
///
/// | Requested | [`Dual`] | [`Single`] | [`None`] |
/// |-----------|----------|------------|----------|
/// | `None` | `None` | `None` | `None` |
/// | `Send` | `Send` | `Send` | `None` |
/// | `Receive` | `Receive` | `Receive` | `None` |
/// | `Both` | `Both` | **`Midpoint`** | `None` |
/// | `Midpoint` | `Midpoint` | `Midpoint` | `None` |
///
/// The one substitution is the observed one: under a single-timestamp
/// allowance a request for both instants is answered with the *midpoint*,
/// not with whichever of the two the server felt like keeping.
///
/// The requested [`Clock`](irtt_proto::Clock) is left exactly as it was in
/// every case. The allowance restricts placement, and the echo layout simply
/// carries no timestamp field once the placement is
/// [`StampAt::None`](irtt_proto::StampAt::None).
///
/// Restriction runs *before* the effective-session check, which is what
/// makes a [`None`] allowance accept a request that selected timestamps
/// without sending a Clock: such a session is refused as requested, because
/// timestamps from no clock cannot be produced, but under this policy its
/// effective placement is none and the absent clock no longer matters.
///
/// [`Dual`]: TimestampAllowance::Dual
/// [`Single`]: TimestampAllowance::Single
/// [`None`]: TimestampAllowance::None
#[must_use]
pub fn with_timestamp_allowance(mut self, timestamp_allowance: TimestampAllowance) -> Self {
self.timestamp_allowance = timestamp_allowance;
self
}
/// Sets whether this server provides the traffic-class marking a session
/// requests.
///
/// `true`, the default, negotiates the requested DSCP parameter unchanged —
/// including a raw value outside `0..=255`, which stays in the negotiated
/// parameters and is transported unmarked.
///
/// `false` negotiates it to **zero**, whatever was requested, so the reply
/// tells the client its replies will be unmarked and the session's stored
/// parameters say the same. The open is never refused over DSCP, and nothing
/// is clamped or wrapped: this is an operator policy about providing
/// traffic-class marking at all, not a judgement about a particular value.
///
/// It is also not socket capability detection. What a given host can apply
/// to a socket is a transport concern, settled per send by the runtime.
#[must_use]
pub fn with_dscp_allowed(mut self, dscp_allowed: bool) -> Self {
self.dscp_allowed = dscp_allowed;
self
}
/// The configured HMAC key, if this server authenticates.
#[must_use]
pub fn hmac_key(&self) -> Option<&[u8]> {
@@ -267,6 +352,19 @@ impl ServerConfig {
pub fn max_test_duration(&self) -> Option<Duration> {
self.max_test_duration
}
/// How many timestamps this server is willing to provide.
#[must_use]
pub fn timestamp_allowance(&self) -> TimestampAllowance {
self.timestamp_allowance
}
/// Whether a requested DSCP value is negotiated as asked, rather than to
/// zero.
#[must_use]
pub fn dscp_allowed(&self) -> bool {
self.dscp_allowed
}
}
impl Default for ServerConfig {
@@ -279,6 +377,11 @@ impl Default for ServerConfig {
/// No maximum test duration is configured, matching the reference server's
/// ordinary policy. A session is still bounded, by the table size and the
/// idle timeout.
///
/// Both capability restrictions are off: every requested timestamp placement
/// is honored ([`TimestampAllowance::Dual`]) and a requested DSCP value is
/// negotiated as asked. They are opt-in, so an existing configuration
/// negotiates exactly what it did before they existed.
fn default() -> Self {
Self {
hmac_key: None,
@@ -288,6 +391,8 @@ impl Default for ServerConfig {
burst_allowance: DEFAULT_BURST_ALLOWANCE,
idle_timeout: DEFAULT_IDLE_TIMEOUT,
max_test_duration: None,
timestamp_allowance: TimestampAllowance::Dual,
dscp_allowed: true,
}
}
}