Commit Graph
18 Commits
Author SHA1 Message Date
Nils Andreas Svee 09f0d6e02e feat(server): add multi-listener server set
`Server` answers on one address. A host that must serve several — an
explicit IPv4 and IPv6 pair, or two interface addresses — needs several
of them, and something has to own their construction, their shutdown and
each other's failures.

`ServerSet` is that owner, and deliberately not a protocol object: it
holds no session state, no tokens and no clock, and nothing it does is
visible on the wire. Each listener stays a complete `Server` with its own
socket, core, session table and tokens, so a token issued by one is
unknown at the others and every bound in the cloned `ServerConfig` —
`max_sessions` included — applies per listener rather than per process.

Construction binds every listener or none, preserving requested order and
recording the addresses actually bound. `run` moves each listener into
its own task, fans one shutdown future out over a watch channel, and
joins them all: a listener that fails, or that returns before shutdown
was asked for, shuts its siblings down and fails the set, because a
service configured for two families must not continue silently as one.

A genuine IPv6 listener sharing an explicit port with an IPv4 sibling is
created `IPV6_V6ONLY` before it is bound, since a dual-stack socket would
otherwise claim the IPv4 port too. That is the only pre-bind step, it
uses safe `socket2` APIs, and an IPv4-mapped address is read as the IPv4
listener it is — never as the IPv6 half of a pair. Standalone
`Server::bind` semantics are unchanged.
2026-08-14 16:04:46 +07:00
Nils Andreas Svee 8607720219 docs(server): describe single allowance as one instant 2026-08-14 15:00:38 +07:00
Nils Andreas Svee 8308c077eb feat(server): add negotiation policy controls 2026-08-14 14:24:57 +07:00
Nils Andreas Svee e4239f45c5 feat(server): generate echo payload fill 2026-08-14 12:58:30 +07:00
Nils Andreas Svee aed7eef454 fix(server): mark a mapped listener with the IPv4 option 2026-08-14 11:49:05 +07:00
Nils Andreas Svee 2d88939ed4 fix(server): pin the reply source, not the route 2026-08-14 11:31:10 +07:00
Nils Andreas Svee 3c554a78d7 test(server): verify wildcard source selection 2026-08-14 00:30:02 +07:00
Nils Andreas Svee 1830429ce5 fix(server): keep unmarkable listeners serving 2026-08-13 22:26:58 +07:00
Nils Andreas Svee 5420387846 feat(server): apply reply traffic class 2026-08-13 20:40:07 +07:00
Nils Andreas Svee 41e39bd5b9 fix(server): keep shutdown responsive during sends 2026-08-13 01:50:25 +07:00
Nils Andreas Svee 4fb5ca6d15 feat(server): add tokio udp runtime 2026-08-13 01:32:05 +07:00
Nils Andreas Svee c2994f64a4 feat(server): enforce rate and session lifetime 2026-08-13 00:45:40 +07:00
Nils Andreas Svee cce2ff731a fix(server): order an echo reply's timestamp pair 2026-08-12 23:47:34 +07:00
Nils Andreas Svee 8abab7ee38 feat(server): implement echo core 2026-08-12 23:21:02 +07:00
Nils Andreas Svee 0e7d363f96 feat(server): bound negotiated packet lengths 2026-08-12 22:25:14 +07:00
Nils Andreas Svee ca2c3dcbd8 fix(server): reject unusable timestamp sessions 2026-08-12 17:21:23 +07:00
Nils Andreas Svee 6c34fd3d20 docs(server): record IPv6 zone identity as project policy
Endpoint identity documented the IPv6 scope identifier as an established
component alongside family, address and port. The server specification does
list the zone, but its remaining-unknowns section records that multi-zone
behavior could not be tested on a single-host platform, so no evidence settles
how a reference server compares scoped addresses. Presenting the choice as
verified compatibility overstated what is known, which the crate's own guidance
forbids.

Say what it is instead: a deliberate policy taken under a recorded unknown. Two
peers reachable at the same link-local address in different zones are genuinely
different peers, so treating the zone as identity is the conservative reading —
the failure it prevents is one peer closing another's session, and the failure
it risks is a scoped client having to reopen. The excluded flow label is
labelled the same way, and `irtt-server` guidance now carries the rule so the
echo slice reuses this comparison rather than inventing a second one.

Behavior is unchanged; this is documentation and a test comment.
2026-08-12 10:48:42 +07:00
Nils Andreas Svee 59f6e88005 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.
2026-08-12 00:47:46 +07:00