Release the session a client close names. A close is bound to a session only when the token matches and the datagram arrived from that session's exact endpoint, so holding a token is not by itself authority to tear the session down: a close from a foreign source port, address, family or IPv6 scope leaves the session live, and one close cannot take down a sibling session the same peer opened separately. A token that is unknown, already closed or zero matches nothing and leaves the table untouched; zero is reserved for no-test replies and never issued. Nothing is sent either way. Protocol version 1 defines no acknowledgement for a client close, so a successful close returns the same `Ok(None)` as a rejected one and a delivered close is indistinguishable to the client from a lost one. A standalone `Reply|Close` datagram is tolerated by clients but has no demonstrated use, so none is encoded. Authentication still runs before the session is looked up. That keeps a bad MAC on a live token indistinguishable from an unknown token, and means a close that fails authentication — missing, wrongly keyed, corrupted, or unexpected on a server with no key — cannot remove a session. Trailing bytes after the token stay acceptable: the structural decoder already tolerates them and the server does not re-parse the request. The session table remains the only record of how full the server is, so a close frees capacity immediately and a refused open can succeed once one is released. Close draws no token and touches no other state; the endpoint lookup borrows the table only until the comparison is made, so the removal needs no clone. Echo remains ignored. The test that pinned close as a no-op is replaced by `tests/close.rs`, which covers removal, silence, repeated close, echo after close, endpoint binding, unknown/zero/stale tokens, authentication failure, trailing bytes, sibling sessions and capacity reuse; `unsupported.rs` keeps echo and structurally invalid traffic.
14 KiB
Repository guidance
Repository-wide guidance for contributors and coding agents working on irtt-rs.
Before editing, also check for a more specific AGENTS.md in the target subtree.
Clean-room boundary
irtt-rs is an independent Rust implementation of an IRTT-compatible protocol stack. It began as a clean-room client reimplementation and now contains project-specific architecture and behavior beyond that original compatibility baseline.
- Do not inspect, quote, copy, translate, or derive implementation details from upstream
heistp/irttsource code or upstream tests. - Do not use contaminated notes or source-derived pseudocode as implementation input.
- Use the local clean-room specification, black-box verification artifacts, current
irtt-rscode, and current tests. - Black-box interoperability testing against an installed upstream
irttbinary is allowed when relevant. Treat it as an external implementation, not source material. - If behavior remains ambiguous, state the ambiguity instead of guessing or consulting upstream implementation code.
Compatibility and interoperability references:
docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md— verified client-side compatibility baseline.docs/protocol/IRTT_SERVER_PROTOCOL_SPEC.md— verified server-side compatibility baseline.docs/protocol/BLACKBOX_VERIFICATION_REPORT.md— black-box evidence and corrections supporting both the client and server baselines.docs/protocol/test-vectors/README.md— captured client-facing packet vectors.docs/protocol/test-vectors/SERVER_BEHAVIORAL_VECTORS.md— black-box server behavioral vectors, edge cases, and session behavior.docs/protocol/captures/— packet captures referenced by the verification report and behavioral vectors. Behavioral vectors and captures are evidence/artifacts, not architecture prescriptions.docs/INTEROP_COMPARISON.md— procedures for black-box comparison against the upstreamirttexecutable.docs/clean-room/CLEANROOM_NOTES.md— provenance only; not a normative protocol specification.
The verified client spec predates later irtt-rs architecture and project-specific features. Its scope statements, including server implementation being out of scope, describe that document's scope and are not permanent repository restrictions.
For compatibility questions, the verified spec and black-box observations define the baseline. For irtt-rs-specific APIs, runtime architecture, resource bounds, and later features, current code and current project documentation are authoritative.
Architecture
Main boundary:
irtt-proto— wire semantics shared by client/server.irtt-client— client/session behavior.irtt-server— server/session behavior and Tokio runtime/orchestration.irtt-stats— statistics over client-facing events/results as currently designed.irtt-cli— presentation/orchestration.
irtt-proto: pure protocol encoding/decoding, flags, layouts, params, varints/zigzag, HMAC-MD5, validation. No sockets, runtimes, lifecycle, stats, or presentation.irtt-client: socket/session lifecycle, negotiation, probe send/receive, timing metadata, classification, bounded state, and events.Client: runtime-free blocking/manual adapter.AsyncClient: optional Tokio low-level adapter.ManagedClientTask/ManagedClientHandle: unified Tokio managed driver/control surface.BlockingManagedClient: synchronous owner using a dedicated current-thread Tokio runtime.
irtt-server: exists; see "Server architecture (irtt-server)" below.irtt-stats: cumulative/rolling/finite statistics over client events. No wire/session state machine logic.irtt-cli: argument parsing, managed orchestration, output, summaries, TUI, diagnostics, and exit behavior. Do not duplicate protocol/session state machines here.
Tokio must remain optional for irtt-client; default builds must remain runtime-free and Tokio-free unless an explicit architectural change says otherwise. This rule is specific to irtt-client — do not generalize it to irtt-server, which has the opposite policy below.
Server architecture (irtt-server)
irtt-server is a first-class reusable server crate. It currently contains the deterministic OPEN/session core — packet admission, authentication policy, open negotiation, the bounded session table, open reply construction and client-initiated close. ECHO processing, server-initiated close and session lifetime remain unimplemented, as does the Tokio runtime; see crates/irtt-server/AGENTS.md. The rest of this section describes the boundary the crate is built to, including the parts not yet written.
- It shares wire semantics with the client through
irtt-protoand follows the same clean-room boundary. - It must not depend on
irtt-clientmerely to reuse protocol behavior. - Unlike
irtt-client, the server has no product requirement to remain runtime-free:irtt-serveris intentionally Tokio-native, and Tokio does not need to be optional for it. - There is currently no requirement for a blocking server API, alternate async runtimes, a runtime abstraction layer, or a runtime-free server feature. Do not build blocking/runtime variants merely for symmetry with the client, and do not invent a mirrored client-style API family (
BlockingServer,AsyncServer,ManagedServer,BlockingManagedServer, or similar) ahead of agreement. - Protocol/session logic should be separated from socket/runtime orchestration where that materially improves deterministic testing, ownership clarity, state-machine reasoning, or maintainability. A
ServerCore-like internal boundary is therefore desirable, but its exact name/API is not prescribed, it need not be public, it is not a promise of runtime independence, and simple code should not be tortured merely to make the core artificially pure. - Runtime/server orchestration belongs around that core and may freely use Tokio for UDP sockets,
recv_from/send_to, IPv4/IPv6 listeners, timers, session expiry, max-duration deadlines, shutdown, bounded control/config channels where useful, andselect!-style orchestration. - The CLI server applet, when added, should remain thin orchestration/configuration over reusable
irtt-server, not contain a second server state machine. - Resource policy: the server should use bounded session/resource policy. Upstream's observed lack of useful default session/per-peer bounds (see
IRTT_SERVER_PROTOCOL_SPEC.md) is not an interoperability requirement and must not be reproduced merely for compatibility. - Testing: prefer
irtt-serverfor normal compliant fake-peer behavior where practical, as far as its implemented behavior reaches. Keep narrow adversarial/raw peers for intentionally malformed, ambiguous, or non-compliant wire behavior.
Engineering rules
- Make the smallest coherent change that solves the requested problem.
- Avoid unrelated cleanup and speculative abstractions.
- Preserve public API and wire behavior unless the task explicitly changes them.
- Treat resource bounds, wake behavior, blocking, cancellation, cleanup, and lifecycle termination as correctness concerns in long-running code.
- Keep fallible preparation before irreversible side effects where transaction semantics require it.
- Do not suppress compiler or Clippy warnings merely to make checks green.
- If an apparent defect may encode an intentional design decision, inspect surrounding code/docs/history and ask rather than silently redesigning it.
Testing policy
Tests protect durable behavior, compatibility, and important invariants. They do not exist to maximize test count or coverage percentage.
Prefer behavior over internals
For client, managed-runtime, blocking, CLI, and TUI behavior, prefer normal-interface component/integration tests: real UDP/session exchange, emitted events/outcomes, negotiated params, multi-target lifecycle, process exit status, and user-visible diagnostics.
One strong behavioral test is usually better than several tests of private helpers that reconstruct the same behavior.
Do not add persistent tests merely because implementation code changed. Avoid tests that primarily:
- assert private fields, cursors, queue shapes, or exact internal representation;
- mirror implementation steps line-for-line;
- exercise branches/helpers solely for coverage;
- duplicate stronger coverage at another layer;
- pin incidental scheduling/ordering that is not contractual;
- test standard-library/dependency behavior;
- require production test-only observability for a fact naturally visible through packets/events/outcomes.
A harmless private refactor should not require widespread test rewrites unless a deliberately protected invariant changed.
Unit tests where they earn it
Focused unit/table-driven tests are encouraged for stable pure or algorithmic behavior, especially:
irtt-protolayouts, codecs, vectors, HMAC, parsing, malformed input, and boundaries;irtt-statsformulas, loss accounting, IPDV, rolling windows, and bounded state;- hand-rolled data structures and subtle pure invariants;
- resource/liveness guarantees that cannot be observed reliably from a higher layer.
If a unit test and a normal-interface test prove the same durable behavior, prefer the normal-interface test unless the unit test materially improves precision or diagnosis.
Temporary implementation tests
Temporary narrow tests are welcome while implementing/debugging a change to prove a hypothesis, reproduce a bug, or exercise a difficult state transition.
Before finalizing, remove or consolidate them unless they independently protect a durable regression or invariant. A test being useful during development does not by itself justify permanent repository code.
Resource/liveness tests and hooks
Direct low-level tests and test-only counters are justified when the invariant is otherwise not observable, e.g. bounded work per poll, no starvation/eventual progress, avoiding an otherwise O(n) hot-path operation, or exact cancellation/stop/update linearization.
Assert the invariant rather than the current mechanism used to achieve it. Prefer “bounded work and eventual progress” over a specific private cursor value when another fair implementation would also be valid.
#[cfg(test)] fault injection is acceptable for conditions that cannot be produced reliably through normal interfaces, such as socket-option failures, short sends, false readiness, exact cancellation points, sequence wrap, or race linearization. Do not add test-only production hooks solely for convenient observability.
Fake peers and interoperability
Use production protocol encoders/decoders for compliant fake peers. Do not maintain shadow packet encoders in tests unless intentionally constructing malformed wire data.
Prefer a real irtt-server for normal compliant behavior once available; keep small adversarial peers for behavior a compliant server should not produce.
Cross-implementation tests are especially valuable because two irtt-rs endpoints can otherwise agree on the same mistake.
When compatibility is in question, prefer black-box interoperability testing
against an upstream irtt executable where practical. Follow
docs/INTEROP_COMPARISON.md and treat upstream only as an external executable;
do not inspect its source or tests.
Synchronization and timing
Prefer synchronization on actual events/state over sleeps or tiny wall-clock windows.
- Use channels, condvars, packet gates, or equivalent deterministic signals with bounded failure timeouts.
- Sleeps are fine when oversleeping cannot invalidate correctness; do not sleep merely hoping another thread/process finished work.
- Avoid wall-clock assertions that include process spawn or unrelated setup time.
- Condition-wait loops must be bounded and fail with a useful diagnostic rather than hang.
When a test fails after a legitimate implementation change, determine whether production regressed or the harness encoded an invalid assumption. Do not change correct production behavior merely to satisfy a brittle test.
Workflow and verification
For non-trivial work:
- Inspect relevant code, tests, and local protocol/design references.
- Identify the externally meaningful contract or internal invariant.
- Make a short plan for multi-file or subtle lifecycle/state changes.
- Implement the smallest coherent patch.
- Run focused verification while iterating.
- Review the final diff for accidental scope, brittle tests, and unnecessary test hooks.
- Run broader verification appropriate to the risk.
Skip planning ceremony for tiny obvious edits. Do not add unrelated tests, abstractions, docs, or refactors merely because they are nearby.
Use the narrowest useful checks first. For substantive workspace changes, final verification should normally include:
cargo fmt --all -- --check
cargo check --workspace --all-targets --all-features --locked
cargo test --workspace --all-targets --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
Also verify --no-default-features when changing feature gates, optional dependencies, runtime boundaries, or code intended to build without Tokio.
Interop tests require an external irtt binary and should only run when the task concerns interoperability or explicitly requests them. Do not run repetition loops, fuzzing, Miri, Loom, mutation testing, or similar expensive checks as routine ceremony.
Never claim a command passed unless it actually ran successfully.
Repository hygiene
- Do not use destructive Git operations unless explicitly requested.
- Do not push, force-push, create/update a PR, or rewrite unrelated history unless explicitly requested.
- Keep commits scoped to the requested work and preserve unrelated local changes.
Completion
Report concisely:
- what changed;
- verification actually run and its result;
- meaningful tests added/updated/consolidated/removed;
- remaining risks, ambiguities, or follow-ups.
Do not produce a giant changelog of incidental edits.