# `irtt-server` guidance Repository-wide guidance in the root `AGENTS.md` also applies here. ## Where server behavior comes from Server wire behavior comes from the sanitized clean server documents, not from upstream implementation material: - `docs/protocol/IRTT_SERVER_PROTOCOL_SPEC.md` - `docs/protocol/test-vectors/SERVER_BEHAVIORAL_VECTORS.md` - `docs/protocol/BLACKBOX_VERIFICATION_REPORT.md` - `docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md` for the shared wire format Never derive behavior from upstream `heistp/irtt` source or tests. If the clean material does not establish a semantic, state the ambiguity instead of guessing. ## Boundaries - Protocol behavior comes from `irtt-proto`. `irtt-server` must **not** depend on `irtt-client` to reuse wire logic; a needed primitive belongs in `irtt-proto`, where both sides get it. - Do not reimplement magic validation, flag rules, HMAC placement, request classification, token/header offsets or parameter parsing here. - No statistics, CLI or presentation concerns. - `SocketAddr` belongs here: endpoint identity is session state. It does not belong in `irtt-proto`. - Endpoint identity is compared field by field, not with `SocketAddr` equality. Family, address and port are established components. The IPv6 flow label is excluded because it identifies no endpoint; the IPv6 zone is included as a deliberate project policy, because the specification records multi-zone behavior as untested and two peers sharing a link-local address in different zones are genuinely different peers. Reuse that one comparison rather than writing a second identity rule, and keep both choices labelled as policy rather than as verified compatibility. ## Core versus runtime - Deterministic protocol and session logic — admission, negotiation, the session table, resource decisions, packet construction — belongs in the core. - Socket and runtime orchestration — `UdpSocket`, IPv4/IPv6 listeners, `recv_from`/`send_to`, timers, expiry, shutdown, `select!`, socket options — belongs around the core. - The server is intentionally **Tokio-native**. Tokio does not need to be optional for this crate. - Do not add a blocking server, an alternate-runtime variant, a transport trait, a runtime abstraction layer, or a mirrored client-style API family. There is no product requirement for any of them, and `ServerCore` being public is not a promise of runtime independence. ## Rejection is silence Malformed, unauthenticated, unsupported and policy-refused input is discarded without a reply and without disturbing any live session. The protocol defines no error reply, no reset and no NACK. Reserve the error type for internal failures on the server's own side; an inbound `ProtoError` must never surface as a server failure. Hostile UDP must leave the server running. Do not let reply behavior reveal which stage rejected a datagram — in particular, an authentication failure must be indistinguishable from an unknown token. ## An acknowledged open must be executable Validate the **final, restricted** parameters before acknowledging an open, not just the requested ones. Once an open passes admission and negotiation, its effective parameters must be safe for the server to run. - Effective non-none `StampAt` with `Clock::Unspecified` is silently rejected. Timestamps from no clock lay out no timestamp field, so the session could not be run. Do **not** synthesize a clock and do **not** rewrite `StampAt` to none: either answers with a session the client did not ask for. This is `irtt-rs` policy for nonconforming input, chosen because the clean evidence records the reference server accepting this combination and then failing on the first echo. A conforming client always sends a clock when it selects timestamps, so conforming traffic is unaffected. - `Clock::Unspecified` alone is valid — it is just an absent tag. Only the combination is refused, and only an absent tag reaches it, since an explicit zero Clock is already out of range for the decoder. - Run the check after negotiation, so that a later policy restricting `StampAt` to none makes an omitted clock safe without this rule changing. - Negative `Length` stays an accepted negotiated value, returned unchanged in the open reply and stored verbatim in the session. It needs no clamping to be executable: `irtt-proto` floors actual echo packet sizing at the required protocol field block, so a negative length behaves as zero does. - A session's echo datagram must fit `ServerConfig::max_packet_length`, whose default is 65,507 bytes. That is `irtt-rs` resource policy, not an interoperability requirement: it is the largest IPv4 UDP payload, a conservative cross-family ceiling that bounds one session's echo buffer to roughly 64 KiB and that no normal client exceeds. It is **not** an MTU, not an upstream default, and says nothing about what a path will carry. Upstream's unlimited default is not a compatibility target. - A requested positive `Length` above the configured maximum is reduced to it during negotiation, so the reply is honest about what the server will emit. Zero and negative values are left exactly as requested. - Capping the parameter is not sufficient. After negotiation, the mandatory echo field block must itself fit the configured maximum or the open is silently refused — statistics and timestamp fields, and authentication's 16 bytes, can exceed a small maximum on their own. Ask `irtt_proto::echo_packet_len` with this server's actual HMAC mode rather than growing a second packet-size rule here; an unrepresentable length from it is a request rejection, not a `ServerError`. - Zero is a valid maximum and is not a synonym for unlimited: it refuses every session, and no-test opens with it, since both are validated against the same effective session. - The echo slice must reuse this same configured maximum to admit inbound echo request datagrams by their received length, before any receive state is mutated. ## Resource policy Total session and resource state must stay bounded. A single unauthenticated datagram creates a session and opens are never deduplicated, so neither an unbounded table nor a remotely chosen packet buffer is defensible. Upstream's observed absence of session and per-peer bounds, and its unlimited default packet length, are explicitly **not** compatibility targets and must not be reproduced. Configured bounds are trusted local policy: an operator may deliberately set an absurd `max_sessions` or `max_packet_length`, and that is their choice. The defaults are what must stay bounded, and no bound gets an unlimited setting. Keep fallible preparation before irreversible state mutation: a reply is prepared before a session is inserted, so an internal failure cannot leave a half-created session. ## Testing - Drive the core with real packets built by the production encoders. Do not maintain shadow encoders for well-formed wire data. - Hand-build packets only where the point is a payload a compliant encoder cannot produce — truncated varints, out-of-range enums, corrupted MACs. - Token generation is the only nondeterministic part; tests inject a scripted source so identity, collisions and allocation failure are assertable. Keep that seam private. - As the server gains behavior, prefer it over fake peers for normal compliant behavior in other crates' tests. Keep small raw/adversarial peers for intentionally malformed or non-compliant wire behavior.