Files
irtt-rs/crates/irtt-server/AGENTS.md
T

7.4 KiB

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.