feat(server): add open session core

Add `irtt-server` with the deterministic protocol and session engine the
upcoming Tokio runtime will call. `ServerCore::handle_datagram` takes a source
endpoint and the bytes of one datagram and returns the bytes to send back, or
nothing. It performs no I/O and keeps no clock, so every decision is a pure
function of its state and that input apart from drawing session tokens.

Rejection is silence. The protocol defines no error reply, no reset and no
NACK, so malformed, unauthenticated and policy-refused datagrams are discarded
without a response and without disturbing any live session; a client tells
"rejected" from "lost" only by timing out. `ServerError` is reserved for
internal failures on the server's own side, so an inbound `ProtoError` never
surfaces as a server failure.

Open handling authenticates before decoding parameters, so a packet that fails
authentication is discarded without parsing untrusted data and no reply
behavior reveals which stage rejected it. Duration and Interval are accepted
when absent and rejected when explicitly present and not positive, which is
what the presence-aware parameter decode is for. Negotiation currently rewrites
only the protocol version; the restriction knobs the specification describes
are deliberately left for their own slices, with one obvious place to add them.

Sessions are bounded to a finite default. A single unauthenticated datagram
creates one and opens are never deduplicated, so an unbounded table is a
liability rather than a compatibility feature; upstream's absence of session
bounds is explicitly not a target. A full table drops new opens silently and
evicts nothing. No-test opens keep working at capacity because they create no
state.

Session tokens come from the operating system's random source, since a token is
the only thing standing between an off-path attacker and a usable session on an
unauthenticated server. A drawn value that is zero or collides with a live
session is redrawn within a finite budget; there is no predictable fallback,
and a colliding token never overwrites the session that holds it. The reply is
encoded before the session is inserted, so an internal failure cannot leave a
half-created session behind.

Echo and close requests pass admission and are then deliberately ignored: this
slice creates sessions, and lifecycle and receive semantics belong to the
slices that implement them. Tests pin that so a half-implemented lifecycle
cannot appear by accident.
This commit is contained in:
Nils Andreas Svee committed 2026-08-12 00:47:46 +07:00
1 parent 93491e7762
commit 59f6e88005
21 files changed
+1744 -4

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
/// Default bound on the number of simultaneously live sessions.
///
/// This is `irtt-rs` resource policy, not an interoperability requirement. The
/// protocol has no rejection response, so a server refuses by staying silent
/// and the client sees an open timeout it already has to tolerate. Upstream was
/// observed to impose no session bound at all; the server specification
/// explicitly records that as an upstream policy choice which a compatible
/// server is not expected to reproduce.
pub const DEFAULT_MAX_SESSIONS: usize = 1024;
/// Configuration for a [`ServerCore`](crate::ServerCore).
///
/// Fields are private and set through consuming builder methods, so later
/// slices can add configuration without breaking construction. Only the
/// settings this OPEN/session slice actually uses exist yet; parameter
/// restriction knobs, lifetime policy and rate limits arrive with the slices
/// that implement them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerConfig {
hmac_key: Option<Vec<u8>>,
max_sessions: usize,
}
impl ServerConfig {
/// Sets the HMAC key, enabling authentication for this server.
///
/// Authentication is global: with a key configured, every request must
/// carry `FLAG_HMAC` and a valid MAC, and every reply is authenticated.
/// Without one, any request carrying `FLAG_HMAC` is dropped.
#[must_use]
pub fn with_hmac_key(mut self, key: impl Into<Vec<u8>>) -> Self {
self.hmac_key = Some(key.into());
self
}
/// Sets the maximum number of simultaneously live sessions.
///
/// Once the table holds this many sessions, a session-creating open is
/// silently dropped; nothing is evicted. Zero refuses every
/// session-creating open while still answering no-test opens, which create
/// no state. There is deliberately no unbounded setting.
#[must_use]
pub fn with_max_sessions(mut self, max_sessions: usize) -> Self {
self.max_sessions = max_sessions;
self
}
/// The configured HMAC key, if this server authenticates.
#[must_use]
pub fn hmac_key(&self) -> Option<&[u8]> {
self.hmac_key.as_deref()
}
/// The maximum number of simultaneously live sessions.
#[must_use]
pub fn max_sessions(&self) -> usize {
self.max_sessions
}
}
impl Default for ServerConfig {
/// An unauthenticated server bounded to [`DEFAULT_MAX_SESSIONS`] sessions.
fn default() -> Self {
Self {
hmac_key: None,
max_sessions: DEFAULT_MAX_SESSIONS,
}
}
}