From 186c6eba97ee81ea6475f39a2aa77e6fc2c7438f Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Mon, 10 Aug 2026 18:54:22 +0700 Subject: [PATCH] docs: add clean-room docs/specs and agent guidance --- AGENTS.md | 178 ++ CLAUDE.md | 4 + crates/irtt-proto/AGENTS.md | 17 + docs/clean-room/CLEANROOM_NOTES.md | 189 +++ docs/protocol/BLACKBOX_VERIFICATION_REPORT.md | 610 +++++++ docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md | 1450 +++++++++++++++++ docs/protocol/captures/basic-session.pcapng | Bin 0 -> 1444 bytes docs/protocol/captures/dscp-46.pcapng | Bin 0 -> 968 bytes docs/protocol/captures/dscp-session.pcapng | Bin 0 -> 1140 bytes docs/protocol/captures/dscp-values.pcapng | Bin 0 -> 1408 bytes docs/protocol/captures/full-session.pcapng | Bin 0 -> 1380 bytes docs/protocol/captures/hmac-session.pcapng | Bin 0 -> 1244 bytes docs/protocol/captures/large-packet.pcapng | Bin 0 -> 3716 bytes docs/protocol/captures/long-sfill.pcapng | Bin 0 -> 3848 bytes docs/protocol/captures/no-test.pcapng | Bin 0 -> 568 bytes docs/protocol/captures/tstamp-none.pcapng | Bin 0 -> 2160 bytes docs/protocol/test-vectors/README.md | 304 ++++ 17 files changed, 2752 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 crates/irtt-proto/AGENTS.md create mode 100644 docs/clean-room/CLEANROOM_NOTES.md create mode 100644 docs/protocol/BLACKBOX_VERIFICATION_REPORT.md create mode 100644 docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md create mode 100644 docs/protocol/captures/basic-session.pcapng create mode 100644 docs/protocol/captures/dscp-46.pcapng create mode 100644 docs/protocol/captures/dscp-session.pcapng create mode 100644 docs/protocol/captures/dscp-values.pcapng create mode 100644 docs/protocol/captures/full-session.pcapng create mode 100644 docs/protocol/captures/hmac-session.pcapng create mode 100644 docs/protocol/captures/large-packet.pcapng create mode 100644 docs/protocol/captures/long-sfill.pcapng create mode 100644 docs/protocol/captures/no-test.pcapng create mode 100644 docs/protocol/captures/tstamp-none.pcapng create mode 100644 docs/protocol/test-vectors/README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7f7c162 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,178 @@ +# 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/irtt` source 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-rs` code, and current tests. +- Black-box interoperability testing against an installed upstream `irtt` binary 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 wire/measurement + compatibility baseline. +- `docs/protocol/BLACKBOX_VERIFICATION_REPORT.md` — black-box observations + resolving ambiguous behavior in the original specification. +- `docs/protocol/test-vectors/README.md` — captured packet vectors. +- `docs/protocol/captures/` — packet captures referenced by the verification + report. +- `docs/INTEROP_COMPARISON.md` — procedures for black-box comparison against + the upstream `irtt` executable. +- `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` implements wire semantics. `irtt-client` implements client/session behavior. `irtt-stats` aggregates events. `irtt-cli` presents and orchestrates them. + +- `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-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. + +A first-class reusable `irtt-server` crate is a valid project direction. It must follow the same clean-room boundary and share wire logic through `irtt-proto`. A future real server may replace compliant fake peers in integration tests; purpose-built adversarial peers remain appropriate for intentionally malformed or non-compliant 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-proto` layouts, codecs, vectors, HMAC, parsing, malformed input, and boundaries; +- `irtt-stats` formulas, 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: + +1. Inspect relevant code, tests, and local protocol/design references. +2. Identify the externally meaningful contract or internal invariant. +3. Make a short plan for multi-file or subtle lifecycle/state changes. +4. Implement the smallest coherent patch. +5. Run focused verification while iterating. +6. Review the final diff for accidental scope, brittle tests, and unnecessary test hooks. +7. 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: + +```sh +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: + +1. what changed; +2. verification actually run and its result; +3. meaningful tests added/updated/consolidated/removed; +4. remaining risks, ambiguities, or follow-ups. + +Do not produce a giant changelog of incidental edits. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..db99fcd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,4 @@ +# Claude Code + +Read and follow `AGENTS.md` before making changes. When working below a subtree +that contains a more specific `AGENTS.md`, read and follow that file as well. diff --git a/crates/irtt-proto/AGENTS.md b/crates/irtt-proto/AGENTS.md new file mode 100644 index 0000000..5d2c9ed --- /dev/null +++ b/crates/irtt-proto/AGENTS.md @@ -0,0 +1,17 @@ +# `irtt-proto` guidance + +Repository-wide guidance in the root `AGENTS.md` also applies here. + +`irtt-proto` is the pure protocol boundary. Keep it free of sockets, runtimes, client/server lifecycle, statistics, and presentation concerns. + +For protocol behavior, consult before changing semantics: + +- `docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md` +- `docs/protocol/BLACKBOX_VERIFICATION_REPORT.md` +- `docs/protocol/test-vectors/README.md` + +Focused unit and table-driven tests are explicitly encouraged in this crate. Exact wire vectors, layout rules, flag combinations, malformed inputs, boundary values, HMAC behavior, and encode/decode round trips are durable compatibility coverage rather than test-count inflation. + +Prefer extending an existing table/vector family when it naturally covers the new case instead of creating many one-off tests. Do not duplicate an exact vector or invariant solely for branch coverage. + +Never derive protocol behavior from upstream `heistp/irtt` source code or upstream tests. If the local verified specification is insufficient, identify the ambiguity rather than guessing or consulting upstream implementation code. diff --git a/docs/clean-room/CLEANROOM_NOTES.md b/docs/clean-room/CLEANROOM_NOTES.md new file mode 100644 index 0000000..d99f407 --- /dev/null +++ b/docs/clean-room/CLEANROOM_NOTES.md @@ -0,0 +1,189 @@ +# Clean-Room Implementation Notes + +## Purpose + +This document records the clean-room boundary for the IRTT client +reimplementation project. The goal is to ensure that the implementation +is developed from a behavioral/protocol specification only, without exposure +to the GPL-licensed upstream source code. + +## Clean-Room Boundary + +- **Contaminated side:** The specification agent (this side) inspected the + upstream source code, documentation, tests, CLI behavior, and release + notes to understand the IRTT protocol. + +- **Clean side:** The implementation agent will receive only the + `IRTT_CLIENT_PROTOCOL_SPEC.md` document. The implementation agent MUST NOT + see the upstream source code. + +## Source Materials Inspected + +The following materials from the upstream IRTT repository (heistp/irtt, +GPLv2) were inspected to produce the protocol specification: + +1. **Documentation files:** + - `README.md` — project overview, FAQ, features, limitations + - `doc/irtt.md` — main man page + - `doc/irtt-client.md` — client man page with CLI options and JSON output format + - `doc/irtt-server.md` — server man page (referenced, not fully read) + - `CHANGES.md` — changelog + +2. **Source files (inspected for protocol behavior only):** + - Protocol version and constants + - Packet format, field layout, magic bytes, flags + - Parameter serialization format (tag-value pairs, varint encoding) + - Connection lifecycle (open, echo, close handshakes) + - Client configuration and defaults + - Server connection handling and parameter restriction logic + - HMAC computation + - Timestamp handling and clock types + - Received stats (count and window) format + - Round-trip data recording and measurement calculations + - Result computation (RTT, OWD, IPDV, loss) + - Error codes and error handling + - Packet test vectors (serialized bytes for known inputs) + - Network layer (IP version, DSCP, socket options) + - Wait/drain behavior + +3. **Test files:** + - Packet serialization tests (used to derive test vectors only; test + code itself was not copied) + +## Statement of Non-Copying + +No upstream source code, comments, or pseudocode were copied into the +specification document (`IRTT_CLIENT_PROTOCOL_SPEC.md`). The specification +describes only externally observable protocol behavior, wire format, and +interoperability requirements. + +Test vectors in the specification were derived from the known inputs and +expected byte sequences found in the upstream test suite. Only the input +parameters and resulting byte arrays were used; no test logic or code was +copied. + +## Post-Drafting Audit + +After the initial specification was drafted, a clean-room audit was +performed. The following issues were identified and corrected: + +1. **Upstream private type names leaked:** "ctoken" and "seqno" appeared + as parenthetical abbreviations in the terminology table. Removed. + +2. **Upstream implementation preference disclosed:** A note about IPv4 + preference in dual-stack resolution revealed an upstream implementation + choice. Removed. + +3. **Go-specific function references:** References to `binary.PutUvarint`, + `binary.PutVarint`, and `encoding/binary` package named the upstream + language's standard library. Replaced with language-neutral descriptions + (LEB128, protobuf-style zigzag). + +4. **Internal scheduling algorithm described:** The send scheduling section + described a specific "snap-to-interval" drift-correction algorithm that + mirrored upstream control flow. Replaced with observable behavior + description (packets sent at approximately ideal times, drift + compensation is implementation-defined). + +5. **Upstream timer algorithm named:** "The upstream uses exponential + averaging of timer errors" revealed an internal implementation choice. + Removed. + +6. **RTT edge case speculated:** The spec asserted that server processing + time exceeding raw RTT should cause subtraction to be skipped — a guard + the upstream does not actually implement. Moved to Open Questions. + +7. **Received window validity asserted normatively:** The behavior of + window=0 as an invalidity sentinel was stated as fact when it should be + verification-required. Softened with reference to Open Questions. + +8. **Go concurrency term leaked:** "goroutine" appeared in the result model + section. Replaced with "operation." + +9. **Upstream test design referenced:** Section 18.2 described what "the + upstream test" was testing internally. Rewritten to describe the test + vector neutrally. + +10. **Upstream constants asserted normatively:** The minimum restricted + interval (1 second) and maximum parameter buffer size (128 bytes) are + internal constants that were stated as protocol requirements. Moved to + Open Questions with verification suggestions. + +11. **Timestamp capture order stated as MUST:** Implementation-level timing + advice was stated as a protocol requirement. Changed to SHOULD with + measurement-accuracy rationale. + +12. **Internal recording structure described:** Section 19.6 described a + "pre-send timestamp" used for send-call-time measurement, which is an + internal recording detail. Rewritten to focus on observable behavior. + +13. **Self-contradictory correction note:** Section 8.2 contained an inline + "Correction:" note that was confusing and revealed the drafting process. + Cleaned up. + +14. **Field ordering lacked verification note:** The critical field order + in Section 8.1.3 was derived from source and stated without any + verification caveat. Added verification note and new Open Question 19.11. + +## Clean-Room Compliance Checklist + +- [x] No upstream source code included. +- [x] No upstream comments quoted. +- [x] No upstream private function names included. +- [x] No upstream private type names included. +- [x] No upstream file/module layout described. +- [x] No pseudocode derived from upstream implementation. +- [x] No implementation-language API design included. +- [x] No Go-specific language references remain. +- [x] No upstream implementation preferences or algorithm choices disclosed. +- [x] Protocol behavior described independently. +- [x] Source-derived constants marked for verification. +- [x] Ambiguous behavior marked for verification (Section 19). +- [x] Black-box tests proposed where needed (Section 17). +- [x] Post-drafting audit completed and documented (this section). +- [x] Second-pass scrub completed (see below). + +## Second-Pass Clean-Room Scrub + +A second independent review was performed on 2026-04-28 by an agent that +had not seen the upstream source. The following issues were identified and +corrected: + +1. **Implementation architecture directive in scope:** Section 2 stated + "Library-first implementation target," which prescribes internal + architecture. Removed; the spec now says only that CLI design is not + prescribed. + +2. **Concurrency architecture leaked:** Section 6.3 described "a separate + receive path processes incoming reply packets concurrently," which + prescribes an internal threading/task model. Rewritten to state the + observable requirement (send and receive occur concurrently) without + prescribing how. + +3. **Source-derived design rationale in echo request:** Section 8.4 + explained that zeroed fields exist to "use the same field layout that + appears in echo replies," which reveals internal design reasoning. + Simplified to state only the observable purpose (reaching the + negotiated packet length). + +4. **Source-exhaustive knowledge parenthetical:** Section 8.6 said + "(currently only ServerFill)" for string-typed values, revealing + knowledge of the complete parameter set from source inspection. + Removed the parenthetical. + +5. **Upstream CLI syntax leaked:** Section 10.10 used `NxD`/`NrD`/`D` + notation for wait variants, which mirrors upstream CLI option syntax. + Replaced with plain-language descriptions. + +6. **Algorithm name disclosed:** Section 12.9 named "Welford's online + algorithm" as a MAY recommendation. This is implementation guidance. + Replaced with "the method of computing running statistics is + implementation-defined." + +7. **Implementation-specific error category:** Section 14 listed + "Allocate results buffer failure," implying a pre-allocated buffer + strategy. Generalized to "Insufficient resources for test parameters." + +8. **Language-specific references in CLEANROOM_NOTES.md:** The Purpose + section named "the Rust implementation" and the checklist mentioned + "No Rust API design." Both replaced with language-neutral phrasing. diff --git a/docs/protocol/BLACKBOX_VERIFICATION_REPORT.md b/docs/protocol/BLACKBOX_VERIFICATION_REPORT.md new file mode 100644 index 0000000..c7b09a0 --- /dev/null +++ b/docs/protocol/BLACKBOX_VERIFICATION_REPORT.md @@ -0,0 +1,610 @@ +# IRTT Black-Box Verification Report + +**Date:** 2026-04-28 +**irtt version:** 0.9.1 (protocol version 1, json format version 1) +**OS:** macOS Darwin 25.3.0 arm64 (Apple Silicon) +**tshark version:** TShark (Wireshark) 4.6.4 +**Loopback interface:** lo0 + +--- + +## Environment Check + +``` +$ command -v irtt +/Users/lochnair/.local/bin/irtt + +$ irtt version +irtt version: 0.9.1 +protocol version: 1 +json format version: 1 +go version: go1.26.2 on darwin/arm64 + +$ command -v tshark +/opt/homebrew/bin/tshark + +$ tshark --version +TShark (Wireshark) 4.6.4 (Git commit f7c4a74874d9). +``` + +--- + +## Open Question Resolution + +### 19.1 Open Request Field Layout — RESOLVED + +**Question:** Does the open request include a connection token field (zeroed)? + +**Test setup:** Captured a full session including open exchange. + +**Command:** +``` +irtt server -b 127.0.0.1:2112 +irtt client -d 3s -i 1s 127.0.0.1:2112 -q +``` + +**Capture:** `captures/full-session.pcapng` + +**Observed open request (frame 1):** +``` +14 a7 5b 01 01 02 02 80 f8 82 ad 16 03 80 a8 d6 +b9 07 05 06 06 06 07 06 +``` +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `01` (Open) +- Bytes 4+: Parameter payload (no connection token) + +**Observed open reply (frame 2):** +``` +14 a7 5b 03 13 52 71 87 ab b6 96 78 01 02 02 80 +f8 82 ad 16 03 80 a8 d6 b9 07 05 06 06 06 07 06 +``` +- Bytes 0-2: Magic +- Byte 3: Flags `03` (Open | Reply) +- Bytes 4-11: Connection token `13 52 71 87 ab b6 96 78` +- Bytes 12+: Parameter payload + +**Conclusion:** The open request does NOT include a connection token field. +The layout is: magic (3) + flags (1) + [HMAC (16) if applicable] + params. +This is a **normative requirement** — the connection token is absent in open +requests because it has not yet been assigned. + +--- + +### 19.2 Minimum Packet Length — RESOLVED + +**Question:** What is the minimum echo packet length for various +configurations? + +**Test setup:** Multiple sessions with different --stats and --tstamp +settings, all with -l 0 (minimum length). + +**Commands:** +``` +irtt client -d 1s -i 1s --tstamp=none --stats=none 127.0.0.1 +irtt client -d 1s -i 1s --tstamp=send --clock=wall --stats=count 127.0.0.1 +irtt client -d 1s -i 1s --tstamp=receive --clock=monotonic --stats=window 127.0.0.1 +irtt client -d 1s -i 1s --tstamp=midpoint --clock=both --stats=both 127.0.0.1 +irtt client -d 1s -i 1s --tstamp=both --clock=both --stats=both 127.0.0.1 +``` + +**Capture:** `captures/tstamp-none.pcapng` + +**Observed minimum packet lengths (echo request/reply):** + +| Stats | StampAt | Clock | Packet Length | Breakdown | +|-------|---------|-------|---------------|-----------| +| none | none | - | 16 bytes | magic(3)+flags(1)+token(8)+seq(4) | +| count | send | wall | 28 bytes | +recv_count(4)+send_wall(8) | +| window | receive | mono | 32 bytes | +recv_window(8)+recv_mono(8) | +| both | midpoint | both | 44 bytes | +recv_count(4)+recv_window(8)+mid_wall(8)+mid_mono(8) | +| both | both | both | 60 bytes | +recv_count(4)+recv_window(8)+recv_wall(8)+recv_mono(8)+send_wall(8)+send_mono(8) | + +With HMAC, add 16 bytes (observed: 76 bytes for default+HMAC). + +**Conclusion:** The minimum packet length formula is: +``` +min_length = 4 (header) + 8 (token) + 4 (seq) + + [16 if HMAC] + + [4 if recv_count (stats=count or both)] + + [8 if recv_window (stats=window or both)] + + [8 per timestamp field present] +``` + +Timestamp field count depends on StampAt and Clock: +- none: 0 fields +- send/wall or receive/wall: 1 field (8 bytes) +- send/mono or receive/mono: 1 field (8 bytes) +- send/both or receive/both: 2 fields (16 bytes) +- both/wall: 2 fields (16 bytes) +- both/mono: 2 fields (16 bytes) +- both/both: 4 fields (32 bytes) +- midpoint/wall: 1 field (8 bytes) +- midpoint/mono: 1 field (8 bytes) +- midpoint/both: 2 fields (16 bytes) + +This is a **normative requirement** — implementations must compute the +correct minimum to avoid malformed packets. + +--- + +### 19.4 Varint Encoding — RESOLVED + +**Question:** Is the varint encoding byte-compatible with protobuf-style +LEB128/zigzag? + +**Test setup:** Decoded parameters from captured open request packets and +verified against known values. + +**Observed parameter encodings:** + +| Parameter | Value | Zigzag Encoded | LEB128 Bytes | +|-----------|-------|---------------|--------------| +| ProtocolVersion=1 | 1 | 2 | `02` | +| Duration=3s | 3000000000 | 6000000000 | `80 f8 82 ad 16` | +| Duration=1s | 1000000000 | 2000000000 | `80 a8 d6 b9 07` | +| Interval=1s | 1000000000 | 2000000000 | `80 a8 d6 b9 07` | +| Length=1472 | 1472 | 2944 | `80 17` | +| ReceivedStats=Both | 3 | 6 | `06` | +| StampAt=Both | 3 | 6 | `06` | +| Clock=Both | 3 | 6 | `06` | +| DSCP=0xb8 | 184 | 368 | `f0 02` | +| ServerFill (tag) | 9 | n/a (uvarint) | `09` | +| ServerFill (length) | 24 | n/a (uvarint) | `18` | + +All decoded correctly using standard protobuf-style zigzag + LEB128. + +**Verified independently with Python decoder.** + +**Conclusion:** The varint encoding is standard protobuf-compatible. This +is a **normative requirement** for interoperability. + +--- + +### 19.5 HMAC Computation Scope — RESOLVED + +**Question:** Is the HMAC computed over the entire packet (including payload) +with the HMAC field zeroed? + +**Test setup:** Captured HMAC session, extracted packet bytes, independently +computed HMAC-MD5 with Python. + +**Commands:** +``` +irtt server -b 127.0.0.1:2114 --hmac=testkey +irtt client -d 2s -i 1s --hmac=testkey 127.0.0.1:2114 -q +``` + +**Capture:** `captures/hmac-session.pcapng` + +**Verification (Python):** +```python +import hmac, hashlib +key = b"testkey" +# Zero HMAC field (bytes 4-19), compute over entire packet +computed = hmac.new(key, packet_with_zeroed_hmac, hashlib.md5).digest() +``` + +**Results:** +| Packet Type | Captured HMAC | Computed HMAC | Match | +|-------------|--------------|--------------|-------| +| Open request | `ff9016a7aa537816...` | `ff9016a7aa537816...` | YES | +| Echo request (seq 0) | `d9874f82b3131031...` | `d9874f82b3131031...` | YES | +| Echo reply (seq 0) | `eb30c07d6375a30b...` | `eb30c07d6375a30b...` | YES | +| Close request | `f5cd0fa9de9d7d66...` | `f5cd0fa9de9d7d66...` | YES | + +Also verified the spec's test vectors (18.1 and 18.2) — both match when +using the correct 92-byte packet size. + +**Conclusion:** HMAC-MD5 is computed over the entire packet buffer with the +16-byte HMAC field zeroed. This is a **normative requirement**. + +**Additional finding:** The test vectors in Section 18 are described as +"target length 256 bytes (shown truncated)" but the HMACs were actually +computed over 92-byte packets (76-byte header + 16-byte payload). The +"256 bytes" description is incorrect in the spec. + +--- + +### 19.7 Server Received Window Validity — RESOLVED + +**Question:** Is bit 0 always set for valid windows, making 0 a sentinel for +invalid? + +**Observed window values from `captures/full-session.pcapng`:** + +| Seq | Received Count | Received Window (hex LE) | Window (binary, low bits) | +|-----|---------------|-------------------------|--------------------------| +| 0 | 1 | `01 00 00 00 00 00 00 00` | ...0001 | +| 1 | 2 | `03 00 00 00 00 00 00 00` | ...0011 | +| 2 | 3 | `07 00 00 00 00 00 00 00` | ...0111 | + +**Analysis:** Bit 0 (LSB) represents the current packet, which was +obviously received (it's being replied to). This bit is always 1 for +valid windows. A window value of 0 would indicate either an invalid window +or a bug. + +**Conclusion:** Bit 0 is always set for valid windows. A window value of 0 +can be used as an invalidity sentinel. This is a **compatibility +recommendation** — implementations SHOULD treat window=0 as invalid. + +--- + +### 19.8 HMAC Mismatch Behavior — RESOLVED + +**Question:** What happens when HMAC keys don't match? + +**Commands:** +``` +irtt server -b 127.0.0.1:2120 --hmac=correctkey +irtt client --hmac=wrongkey --timeouts=500ms 127.0.0.1:2120 +irtt client --timeouts=500ms 127.0.0.1:2120 # no HMAC +``` + +**Observed behavior:** + +| Scenario | Server Log | Client Result | +|----------|-----------|--------------| +| Wrong HMAC key | `[Drop] [BadHMAC] invalid HMAC: ...` | `[OpenTimeout] no reply from server` (exit 1) | +| No HMAC to HMAC server | `[Drop] [NoHMAC] no HMAC present` | `[OpenTimeout] no reply from server` (exit 1) | + +**Conclusion:** The server silently drops packets with missing or invalid +HMAC. The client times out. This is **observed behavior** — the protocol does +not define an explicit HMAC error response. + +--- + +### 19.9 Minimum Restricted Interval Safety Floor — PARTIALLY RESOLVED + +**Question:** What safety floor does the client enforce for server-restricted +intervals? + +**Test setup:** Server with -i 500ms (minimum interval), client requests +200ms. + +**Command:** +``` +irtt server -b 127.0.0.1:2123 -i 500ms +irtt client -d 2s -i 200ms 127.0.0.1:2123 +``` + +**Observed:** `[ServerRestriction] server increased interval from 200ms to +500ms` — client exits with code 1 in strict mode. In loose mode, client +accepts 500ms interval. + +**Limitation:** This test only confirms that the client rejects server- +increased intervals in strict mode. It does not confirm the exact safety +floor value for server-decreased intervals (which would require a server +that decreases interval below the client's request). The safety floor for +decreased intervals cannot be easily tested without a cooperative server +that sends deliberately small intervals. + +**Conclusion:** Strict mode rejects any server parameter change by default. +The specific safety floor for decreased intervals remains an **open +question** that requires further testing. + +--- + +### 19.10 Maximum Parameter Buffer Size — PARTIALLY RESOLVED + +**Question:** Is there a maximum serialized parameter payload size? + +**Test setup:** Open request with all parameters including a 24-character +ServerFill string. + +**Observed:** Largest serialized parameter payload was 52 bytes (with +ProtocolVersion, Duration, Interval, Length=1472, ReceivedStats=both, +StampAt=both, Clock=both, DSCP=0xb8, ServerFill="pattern:aabbccddeeff0011"). + +**Analysis:** Even with maximum-value parameters and the longest reasonable +ServerFill string (32 bytes max), the parameter buffer stays well under +128 bytes. A rough upper bound: +- Tag+value overhead per param: ~6 bytes for large values +- 8 params × 6 bytes = 48 bytes +- Plus ServerFill: tag(1) + length(1) + string(32) = 34 bytes +- Total: ~82 bytes maximum + +**Conclusion:** The 128-byte limit stated in the spec appears safe. +Reclassified as **compatibility recommendation** — implementations should +allocate at least 128 bytes for the parameter buffer. + +--- + +### 19.11 Field Ordering Verification — RESOLVED + +**Question:** Is the field ordering in Section 8.1.3 correct? + +**Test setup:** Multiple captures with different field combinations. + +**Verified orderings:** + +1. **No optional fields (stats=none, tstamp=none):** + Token → Seq. Packet = 16 bytes. ✓ + +2. **Count + send wall:** + Token → Seq → Recv Count → Send Wall. Packet = 28 bytes. ✓ + +3. **Window + receive mono:** + Token → Seq → Recv Window → Recv Mono. Packet = 32 bytes. ✓ + +4. **Both + midpoint both:** + Token → Seq → Recv Count → Recv Window → Midpoint Wall → Midpoint Mono. + Packet = 44 bytes. ✓ + +5. **Both + both both (default):** + Token → Seq → Recv Count → Recv Window → Recv Wall → Recv Mono → + Send Wall → Send Mono. Packet = 60 bytes. ✓ + +6. **HMAC + both + both both:** + HMAC → Token → Seq → Recv Count → Recv Window → Recv Wall → Recv Mono → + Send Wall → Send Mono. Packet = 76 bytes. ✓ + +**Conclusion:** The field ordering in Section 8.1.3 is correct. This is a +**normative requirement** — incorrect ordering breaks interoperability. + +--- + +### 19.3 Server Close During Test — NOT DIRECTLY TESTED + +**Limitation:** Triggering a server-initiated close mid-test requires +specific server conditions that are difficult to set up reliably in a +black-box test. The spec's description (server MAY set Close flag on echo +reply) has not been verified externally. Reclassified as **open / +verification required** — the spec now recommends defensive handling +without asserting that this behavior occurs. + +--- + +### 19.6 Send Timestamp Capture Timing — NOT TESTABLE + +**Limitation:** The exact timing of client-side timestamp capture relative +to the send syscall is an implementation-internal detail that cannot be +verified through black-box testing. Remains an implementation choice. + +--- + +### 19.12 RTT When Server Processing > Raw Round-Trip — NOT DIRECTLY TESTED + +**Limitation:** This edge case requires server processing time to exceed +the raw client-measured RTT, which is extremely unlikely on localhost. Would +require a heavily loaded server or deliberate delays. Remains an +**open question**. + +--- + +## Additional Findings + +### Finding A: Packet Count Formula Correction + +**Spec claim:** Expected packet count = `floor(d / i) + 1` + +**Test setup:** Multiple sessions with various duration/interval combinations. + +**Command:** `irtt client -d -i 127.0.0.1 -q` + +**Results:** + +| Duration | Interval | Observed Packets | floor(d/i)+1 | ceil(d/i) | +|----------|----------|-----------------|-------------|-----------| +| 10s | 200ms | 50 | 51 | 50 | +| 1s | 200ms | 5 | 6 | 5 | +| 1s | 333ms | 4 | 4 | 4 | +| 1s | 500ms | 2 | 3 | 2 | +| 1s | 1s | 1 | 2 | 1 | +| 2s | 1s | 2 | 3 | 2 | +| 500ms | 100ms | 5 | 6 | 5 | +| 100ms | 10ms | 10 | 11 | 10 | +| 999ms | 1s | 1 | 1 | 1 | +| 1001ms | 1s | 2 | 2 | 2 | + +**Analysis:** When duration is an exact multiple of interval, the packet at +exactly `start + duration` is NOT sent (exclusive end). The correct formula +is: + +``` +expected_packets = ceil(duration / interval) +``` + +Or equivalently: +``` +if duration % interval == 0: + expected_packets = duration / interval +else: + expected_packets = floor(duration / interval) + 1 +``` + +**Conclusion:** The spec's formula `floor(d / i) + 1` is incorrect. The +correct formula is `ceil(d / i)`. The spec MUST be updated. + +--- + +### Finding B: DSCP Parameter Encodes TOS Byte Value + +**Spec claim:** DSCP valid values are "0-63 (6-bit DSCP field)" + +**Test setup:** Captured packets with various --dscp values. + +**Results:** + +| CLI Option | TOS Byte in IP Header | DSCP (6-bit) | Param Value Encoded | +|------------|----------------------|-------------|-------------------| +| --dscp=0xb8 | 0xb8 | 46 (EF) | 184 | +| --dscp=0x2e | 0x2e | 11 | 46 | +| --dscp=46 | 0x2e | 11 | 46 | +| --dscp=8 | 0x08 | 2 | 8 | +| --dscp=0x20 | 0x20 | 8 (CS1) | 32 | + +**Conclusion:** The DSCP parameter encodes the raw value as specified on the +CLI, which is used directly as the TOS/Traffic Class byte in the IP header. +The range is 0-255, not 0-63. The spec MUST be updated to reflect that +the parameter is the TOS byte value, not the 6-bit DSCP field. + +--- + +### Finding C: DSCP Not Applied to Open/Close Packets + +**Observation:** In all captures, the IP TOS field for open request, open +reply, and close request packets is always 0x00, regardless of the +negotiated DSCP value. Only echo request and echo reply packets carry the +negotiated DSCP/TOS value. + +**Verified with:** `captures/dscp-session.pcapng` + +**Conclusion:** This is **observed behavior**. It may not be a strict +protocol requirement, but implementations should be aware that DSCP is only +applied during the active test phase. + +--- + +### Finding D: No-Test Mode Packet Exchange + +**Test setup:** Captured a no-test session. + +**Command:** `irtt client -n 127.0.0.1:2113` + +**Capture:** `captures/no-test.pcapng` + +**Observed:** + +Open request (frame 1): +``` +14 a7 5b 05 [params...] +``` +- Flags: `05` = Open (0x01) | Close (0x04) +- No connection token + +Open reply (frame 2): +``` +14 a7 5b 07 00 00 00 00 00 00 00 00 [params...] +``` +- Flags: `07` = Open (0x01) | Reply (0x02) | Close (0x04) +- Connection token: `00 00 00 00 00 00 00 00` (zero) + +**Conclusion:** In no-test mode: +- Client sets both Open and Close flags in the open request. +- Server replies with Open|Reply|Close and a **zero** connection token. +- The client treats this as successful completion, not rejection. +- The Close flag in the open reply is **not** a rejection when the client + originally requested Close. + +The spec's description of "An open reply with the close flag set: the +server rejected the session" needs clarification. The Close flag in the +reply indicates rejection ONLY when the client did not request Close in its +open request. + +--- + +### Finding E: Test Vector Size Correction + +The test vectors in Section 18 are described as "target length 256 bytes +(shown truncated)." Independent HMAC verification confirms that both test +vectors (18.1 and 18.2) were computed over 92-byte packets (76-byte header ++ 16-byte payload), not 256-byte packets. The "256 bytes" description is +incorrect. + +--- + +### Finding F: Default Server Fill Pattern + +**Observation:** When no server fill is requested, the server fills reply +payloads with the repeating hex pattern `69 72 74 74` (ASCII "irtt"). This +is the default server fill as documented in server help: +`--fill=fill payload fill if not requested (default pattern:69727474)`. + +--- + +### Finding G: Close Packet Minimal Format + +**Observed close packet:** +``` +14 a7 5b 04 [8-byte connection token] +``` +Total: 12 bytes. Contains only magic (3) + flags (1) + connection token (8). +No sequence number, no payload. + +With HMAC: +``` +14 a7 5b 0c [16-byte HMAC] [8-byte connection token] +``` +Total: 28 bytes. + +--- + +### Finding H: Unreachable Server Behavior + +**Command:** `irtt client --timeouts=200ms,200ms 127.0.0.1:2121` (no server) + +**Observed:** `Error: read udp4 ... connection refused` (exit 1) + +On localhost, the OS delivers ICMP port unreachable immediately. On remote +hosts, the behavior would be a timeout after all retransmissions are +exhausted. + +--- + +### Finding I: Parameter Restriction Behavior + +**Commands:** +``` +irtt server -b 127.0.0.1 -l 100 # max length 100 +irtt client -l 200 127.0.0.1 # request length 200 +``` + +**Strict mode (default):** `[ServerRestriction] server reduced length from +200 to 100` — exit code 1. + +**Loose mode (--loose):** `[ServerRestriction] server reduced length from +200 to 100` — continues with length 100, exit code 0. + +--- + +## Captures Created + +| File | Contents | +|------|----------| +| `captures/full-session.pcapng` | Complete session: open, 3 echoes, close | +| `captures/basic-session.pcapng` | Basic session (partial, missed open) | +| `captures/no-test.pcapng` | No-test mode: open+close only | +| `captures/hmac-session.pcapng` | HMAC authenticated session | +| `captures/tstamp-none.pcapng` | Multiple sessions with different timestamp modes | +| `captures/dscp-session.pcapng` | DSCP=0xb8 session | +| `captures/dscp-46.pcapng` | DSCP=46 and DSCP=0x2e sessions | +| `captures/dscp-values.pcapng` | DSCP=8 and DSCP=0x20 sessions | +| `captures/large-packet.pcapng` | 1472-byte packet session | +| `captures/long-sfill.pcapng` | Session with long server fill string | + +## Test Vectors Created + +See `test-vectors/` directory. + +## Summary + +### Resolved Open Questions +- **19.1** Open request field layout (no token) +- **19.2** Minimum packet length calculation +- **19.4** Varint encoding compatibility +- **19.5** HMAC computation scope +- **19.7** Received window validity (bit 0 sentinel) +- **19.8** HMAC mismatch behavior +- **19.11** Field ordering verification + +### Partially Resolved +- **19.9** Minimum restricted interval safety floor (strict mode rejects, + but exact floor for decreased intervals untested) +- **19.10** Maximum parameter buffer size (observed well under 128 bytes) + +### Still Unresolved +- **19.3** Server close during test (hard to trigger) +- **19.6** Send timestamp capture timing (implementation detail, not testable) +- **19.12** RTT when server processing exceeds raw RTT (hard to trigger) + +### Spec Corrections Applied +1. Packet count formula: `ceil(d/i)` not `floor(d/i)+1` +2. DSCP parameter: TOS byte value (0-255) not 6-bit DSCP (0-63) +3. Test vector size: 92 bytes not 256 bytes +4. No-test mode close flag: clarified in Sections 6.2, 8.3, and 14 +5. Server close during test (19.3): reclassified as open/verification + required; removed source-derived claims; spec now recommends + defensive handling without asserting the behavior occurs +6. Received window late-packet behavior: removed unverified claim about + late packets producing window=0; retained verified bit-0 sentinel diff --git a/docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md b/docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md new file mode 100644 index 0000000..d53fb34 --- /dev/null +++ b/docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md @@ -0,0 +1,1450 @@ +# IRTT-Compatible Client Protocol Specification + +**Version:** 1.1-verified +**Date:** 2026-04-28 +**Verified against:** irtt 0.9.1, protocol version 1, macOS Darwin 25.3.0 arm64 + +--- + +## 1. Status + +This document describes the externally observable behavior required for an +independent client implementation compatible with existing IRTT (Isochronous +Round-Trip Tester) servers. + +This document does **not** describe upstream source structure, internal +algorithms, or implementation organization. It is intended for a clean-room +implementation that has never seen the upstream source code. + +The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT", +"RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted +as interoperability requirements, consistent with RFC 2119. + +--- + +## 2. Scope + +- **Client-only implementation.** Server implementation is out of scope unless + server behavior is needed to understand client interoperability. +- **Compatible with existing IRTT servers** running protocol version 1. +- This specification describes protocol and measurement behavior; CLI design + is not prescribed. +- Server implementation is out of scope. + +--- + +## 3. Terminology + +| Term | Definition | +|------|-----------| +| **Client** | The endpoint that initiates a session, sends test packets, and receives replies. | +| **Server** | The endpoint that accepts sessions and echoes test packets back to the client. | +| **Session** | A stateful association between a client and server, identified by a connection token. | +| **Open / Negotiation phase** | The initial handshake in which the client proposes test parameters and the server responds with (possibly restricted) parameters and a connection token. | +| **Test packet** | A UDP datagram sent by the client during the active test phase. Sometimes called an "echo request." | +| **Reply packet** | A UDP datagram sent by the server in response to a test packet. Sometimes called an "echo reply." | +| **Close / Finalization phase** | The exchange in which the client notifies the server that the session is ending. | +| **Connection token** | A 64-bit value assigned by the server during negotiation, used to identify the session in all subsequent packets. | +| **Round-trip time (RTT)** | The time between sending a test packet and receiving its reply, optionally minus server processing time. | +| **Send interval** | The fixed time period between successive test packet transmissions. | +| **Sequence number** | A 32-bit unsigned integer, starting at 0, assigned to each test packet in order of transmission. | +| **Timeout** | The duration the client waits for a server reply during the open phase before retransmitting. | +| **Packet loss** | A test packet for which no reply was received. | +| **Duplicate** | A reply received with a sequence number for which a reply was already recorded. | +| **Late packet** | A reply received with a sequence number lower than the most recently received sequence number (indicates out-of-order delivery). | +| **IPDV (Instantaneous Packet Delay Variation)** | The difference in delay between two successive successfully returned replies. Commonly called "jitter." | +| **Server processing delay** | The time the server spent between receiving a request and sending its reply, measured by server timestamps. | +| **Received count** | A running count of all packets the server has received for this session. | +| **Received window** | A 64-bit bitmap indicating the receipt status of the most recent 64 packets at the server. | +| **Wall clock** | System real-time clock (nanoseconds since Unix epoch). Subject to NTP adjustments. | +| **Monotonic clock** | A clock that only moves forward and is not subject to system time adjustments. Used for duration calculations. | +| **HMAC** | Hash-based Message Authentication Code (MD5), used for packet authentication. | +| **DSCP** | Differentiated Services Code Point, set in the IP header TOS/Traffic Class field. | +| **Drain period** | The period after the last test packet is sent during which the client waits for outstanding replies. | + +--- + +## 4. Conformance + +A conforming client MUST implement: + +1. Session lifecycle: open, test, close. +2. The binary wire format defined in this document. +3. Parameter negotiation. +4. Connection token handling. +5. Sequence number assignment and reply matching. +6. RTT measurement. +7. Packet loss detection. +8. Tolerance for UDP reordering and duplication. + +A conforming client SHOULD implement: + +1. HMAC authentication. +2. Server timestamp handling (one-way delay, IPDV). +3. Received stats (count and window) processing. +4. Configurable DSCP. + +Implementations are not required to use any specific internal architecture, +data structure, function layout, naming convention, concurrency model, or +source organization. Only the externally observable behavior described in +this document is normative. + +--- + +## 5. Transport Requirements + +### 5.1 Transport Protocol + +IRTT uses **UDP** exclusively. There is no TCP fallback. + +### 5.2 Default Port + +The default server port is **2112**. The default client local port is +ephemeral (OS-assigned, port 0). + +### 5.3 IPv4 and IPv6 + +A conforming client MUST support IPv4. IPv6 support is RECOMMENDED. When the +user does not specify an IP version preference, the client SHOULD attempt +address resolution for both IPv4 and IPv6, and use whichever resolves +successfully. If both resolve, the selection behavior is +implementation-defined. + +The client MUST use a single connected UDP socket for the entire session. The +client calls the equivalent of `connect()` on the socket, binding it to the +resolved server address before the open phase. + +### 5.4 Socket Options + +- **DSCP/TOS:** A client SHOULD support setting the DSCP value on outgoing + packets. For IPv4, this is the TOS field. For IPv6, this is the Traffic + Class field. The DSCP value is negotiated as a parameter; if the server + disallows DSCP, the negotiated value will be 0. +- **TTL/Hop Limit:** A client MAY support setting the IP TTL (IPv4) or Hop + Limit (IPv6). +- **Don't Fragment (DF):** A client MAY support setting the DF bit. +- **ECN:** ECN bit handling is OPTIONAL and experimental. + +### 5.5 Connectionless Behavior + +Although the client uses a connected UDP socket, the protocol is +connectionless at the network layer. The client MUST tolerate: + +- Packet reordering. +- Packet duplication. +- Packet loss. +- Receiving unrelated UDP traffic on the socket (which MUST be discarded + based on validation rules). + +### 5.6 MTU Considerations + +Common maximum unfragmented UDP payload sizes for a 1500-byte MTU: +- IPv4: 1472 bytes (1500 - 20 IPv4 header - 8 UDP header) +- IPv6: 1452 bytes (1500 - 40 IPv6 header - 8 UDP header) + +The client does not need to perform path MTU discovery. Packet length is a +user-configured parameter. + +--- + +## 6. Session Lifecycle + +A client session progresses through the following externally observable states: + +``` + Resolving ──► Opening ──► Active Test ──► Draining ──► Closing ──► Completed + │ │ + ▼ ▼ + Failed Completed +``` + +### 6.1 Resolving + +- **Entry:** Client is given a server address (hostname or IP) and port. +- **Behavior:** The client resolves the address to a UDP endpoint. The + default port (2112) MUST be appended if no port is specified. +- **Exit:** Successful resolution transitions to Opening. Failure transitions + to Failed. + +### 6.2 Opening / Negotiating + +- **Entry:** The client has a resolved server address and a connected UDP socket. +- **Behavior:** + 1. The client constructs an open request packet (see Section 8.2) containing + the proposed test parameters serialized in the payload. + 2. The client sends the open request and waits for a reply. + 3. If no reply is received within the current timeout, the client + retransmits the open request with the next timeout value from its + timeout list. + 4. The default timeout sequence is: 1s, 2s, 4s, 8s (total maximum wait: + 15s, maximum 4 packets). The minimum timeout value MUST be at least + 200ms. + 5. If all timeouts are exhausted with no reply, the session transitions to + Failed. + +- **Valid server responses:** + - An open reply with a non-zero connection token: session parameters are + extracted from the reply payload. The client stores the connection token. + Transitions to Active Test (or Completed if the no-test flag was set). + - An open reply with the close flag set, **when the client did NOT set + Close in its request**: the server rejected the session (e.g., protocol + version mismatch). Transition to Failed. + - An open reply with the close flag set, **when the client DID set Close + in its request** (no-test mode): the server acknowledged the open+close. + The connection token is zero. Transition to Completed. + [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding D.] + +- **Parameter negotiation:** The server MAY restrict parameters. The client + MUST compare returned parameters against its requested parameters. A + conforming client MUST, by default, reject the session if parameters were + restricted (transition to Failed with an appropriate error). A "loose" mode + MAY be provided that accepts server restrictions with a warning. + + The client MUST reject (as an error) the following server parameter changes: + - Duration increased beyond what was requested. + - Length increased beyond what was requested. + - Interval reduced below a safety floor (see Section 19.9 for verification). + +- **Protocol version check:** The client MUST verify that the server's + protocol version matches exactly. Protocol version 1 is the current + version. Mismatch MUST cause the session to fail. + +### 6.3 Active Test + +- **Entry:** Open reply received and parameters accepted. +- **Behavior:** + 1. The client sends test packets at fixed intervals for the configured + duration. + 2. The client MUST be able to receive reply packets while continuing + to send test packets (i.e., sending and receiving occur concurrently). + 3. Test packets are numbered with sequence numbers starting at 0. + 4. The test duration is **exclusive**: the client MUST NOT send a packet at + or after the end time. The end time is calculated as start time + duration. + The next scheduled send time is computed, and if it would be at or past + the end time, sending stops. + 5. The expected number of packets for a given duration `d` and interval `i` + is: `ceil(d / i)`. [**Verified 2026-04-28** by black-box testing with + irtt 0.9.1. See BLACKBOX_VERIFICATION_REPORT.md Finding A.] + +- **Exit:** All scheduled packets have been sent. Transition to Draining. + +### 6.4 Draining Replies + +- **Entry:** All test packets have been sent. +- **Behavior:** The client waits for outstanding replies. The wait duration + is configurable: + - Default: 3 times the maximum observed RTT, falling back to 4 seconds if + no RTT has been measured. + - Alternatives: a factor of mean RTT, or a fixed duration. + - If all replies have already been received, no wait is needed. + +- **Exit:** Wait period expires, or all replies received. Transition to Closing. + +### 6.5 Closing + +- **Entry:** Test and drain are complete. +- **Behavior:** The client sends a single close packet (see Section 8.4) to + the server. The close packet includes the connection token and the close + flag. The client does NOT wait for a close acknowledgement (this is a + known limitation of protocol version 1). + +- **Exit:** Close packet sent. Transition to Completed. + +### 6.6 Completed + +- **Entry:** Session has finished normally. +- **Behavior:** Results are computed and made available. + +### 6.7 Failed + +- **Entry:** An unrecoverable error occurred. +- **Behavior:** Partial results MAY be available if the failure occurred + during or after the Active Test phase. + +--- + +## 7. Packet Format Overview + +All IRTT packets share a common header structure. The presence or absence of +optional fields is determined by context (packet type and negotiated +parameters). + +| Packet Type | Direction | When Sent | Required | Purpose | +|------------|-----------|-----------|----------|---------| +| Open Request | Client → Server | Opening phase | Yes | Propose session parameters | +| Open Reply | Server → Client | Opening phase | Yes | Return connection token and (possibly restricted) parameters | +| Echo Request (Test Packet) | Client → Server | Active test | Yes | Carry test payload with sequence number | +| Echo Reply | Server → Client | Active test | Yes | Return test payload with timestamps and stats | +| Close Request | Client → Server | Closing phase | Yes | Signal session end | +| Close Reply | Server → Client | Closing phase | No | Acknowledge close (server MAY send this) | + +--- + +## 8. Binary Wire Format + +### 8.1 General Header Structure + +All multi-byte integer fields use **little-endian** byte order. + +The packet format uses a flexible field layout. Fields are placed sequentially +in the packet buffer. A field is either present at its full capacity or absent +(zero length). The set of present fields is determined by the packet type and +negotiated parameters. + +#### 8.1.1 Fixed Header Fields (always present) + +| Offset | Size (bytes) | Field | Encoding | Description | +|--------|-------------|-------|----------|-------------| +| 0 | 3 | Magic | Raw bytes | Fixed value: `0x14 0xA7 0x5B` | +| 3 | 1 | Flags | Unsigned byte | Bit flags (see below) | + +**Total minimum header: 4 bytes.** + +#### 8.1.2 Flag Bits + +| Bit | Mask | Name | Meaning | +|-----|------|------|---------| +| 0 | 0x01 | Open | Set on open request and open reply packets | +| 1 | 0x02 | Reply | Set on all packets from server to client; clear on all packets from client to server | +| 2 | 0x04 | Close | Set on close request and close reply packets | +| 3 | 0x08 | HMAC | Set if an HMAC field is present in the packet | + +Bits 4-7 are reserved and MUST be zero. A client MUST reject packets where +reserved flag bits are set. + +#### 8.1.3 Optional Fields + +After the fixed header, optional fields appear **in the following order** when +present. A field is either present at full size or entirely absent. There are +no length-prefix or presence-indicator bytes; the receiver knows which fields +are present based on the packet type and negotiated session parameters. + +**Verification note:** The field ordering below is critical for +interoperability. [**Verified 2026-04-28** by packet capture with multiple +field combinations — see BLACKBOX_VERIFICATION_REPORT.md Section 19.11.] + +| Order | Field | Size (bytes) | Encoding | When Present | +|-------|-------|-------------|----------|-------------| +| 1 | HMAC | 16 | MD5 HMAC | When HMAC flag (0x08) is set | +| 2 | Connection Token | 8 | Little-endian uint64 | All packets except initial open request sent from client before token is known. Present in open reply and all subsequent packets. | +| 3 | Sequence Number | 4 | Little-endian uint32 | Echo request and echo reply packets | +| 4 | Received Count | 4 | Little-endian uint32 | Echo reply, when "count" stats negotiated | +| 5 | Received Window | 8 | Little-endian uint64 | Echo reply, when "window" stats negotiated | +| 6 | Receive Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when receive timestamps negotiated with wall clock | +| 7 | Receive Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when receive timestamps negotiated with monotonic clock | +| 8 | Midpoint Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when midpoint timestamp negotiated with wall clock | +| 9 | Midpoint Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when midpoint timestamp negotiated with monotonic clock | +| 10 | Send Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when send timestamps negotiated with wall clock | +| 11 | Send Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when send timestamps negotiated with monotonic clock | + +**Important:** Midpoint timestamps are **mutually exclusive** with +receive/send timestamps. If a midpoint timestamp is present, receive and send +timestamp fields MUST NOT be present, and vice versa. A packet containing +both midpoint and receive/send timestamps is malformed. + +When both receive and send timestamps are present, they MUST use the same +clock mode (both wall, both monotonic, or both wall+monotonic). + +#### 8.1.4 Payload + +Everything after the last present header field is **payload**. The total +packet length is the negotiated packet length. If the negotiated length is +larger than the header, the remaining bytes are payload. If the negotiated +length is 0 or unspecified, the packet length equals the header length (no +payload). + +The client fills the payload with zeros, random data, or a repeating +pattern, as configured. The server fills reply payloads according to its +configuration and the negotiated server fill parameter. + +### 8.2 Open Request Packet + +**Direction:** Client → Server +**Flags:** Open (0x01) set. Reply (0x02) clear. HMAC (0x08) set if HMAC key +configured. + +**Header fields present:** +- Magic (3 bytes) +- Flags (1 byte) +- HMAC (16 bytes) — if HMAC configured + +The connection token field is NOT present in the open request (the token has +not yet been assigned). [**Verified 2026-04-28** by packet capture — see +BLACKBOX_VERIFICATION_REPORT.md Section 19.1.] + +**Payload:** Serialized parameters (see Section 8.6). + +If the client wants to open and immediately close (no-test mode), both the +Open (0x01) and Close (0x04) flags are set. + +### 8.3 Open Reply Packet + +**Direction:** Server → Client +**Flags:** Open (0x01) set. Reply (0x02) set. Close (0x04) set if the server +is rejecting the connection. HMAC (0x08) set if applicable. + +**Header fields present:** +- Magic (3 bytes) +- Flags (1 byte) +- HMAC (16 bytes) — if applicable +- Connection Token (8 bytes) + +**Payload:** Serialized parameters (see Section 8.6), reflecting any server +restrictions. + +**Validation:** +- If the Close flag is NOT set, the connection token MUST be non-zero. A + zero token without the close flag is an error. +- If the Close flag IS set **and the client did NOT set Close in its open + request**, the server is rejecting the session (transition to Failed). +- If the Close flag IS set **and the client DID set Close in its open + request** (no-test mode), the server acknowledged the open+close. The + connection token is zero. Transition to Completed. + [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding D.] + +### 8.4 Echo Request Packet (Test Packet) + +**Direction:** Client → Server +**Flags:** Reply (0x02) clear. HMAC (0x08) if applicable. Open (0x01) and +Close (0x04) clear. + +**Header fields present:** +- Magic (3 bytes) +- Flags (1 byte) +- HMAC (16 bytes) — if applicable +- Connection Token (8 bytes) +- Sequence Number (4 bytes) +- Received Count (4 bytes, zeroed) — if "count" stats negotiated +- Received Window (8 bytes, zeroed) — if "window" stats negotiated +- Timestamp fields (zeroed) — according to negotiated stamp_at and clock + +The echo request includes zeroed placeholder fields for received stats and +timestamps so that the total packet reaches the negotiated length. + +**Payload:** Filled according to client configuration (zeros, random, or +pattern). + +**Total packet length:** The negotiated `length` parameter. If the header +(including zeroed optional fields) is larger than the negotiated length, the +actual packet length is the header length. The final negotiated length +reflects this minimum. + +### 8.5 Echo Reply Packet + +**Direction:** Server → Client +**Flags:** Reply (0x02) set. HMAC (0x08) if applicable. Close (0x04) MAY be +set (see Section 19.3 — not yet verified by black-box testing). + +**Header fields present:** +- Magic (3 bytes) +- Flags (1 byte) +- HMAC (16 bytes) — if applicable +- Connection Token (8 bytes) +- Sequence Number (4 bytes) +- Received Count (4 bytes) — if negotiated, contains server's running count +- Received Window (8 bytes) — if negotiated, contains 64-bit received bitmap +- Timestamp fields — according to negotiated stamp_at and clock settings + +**Payload:** Server-filled according to negotiation. + +### 8.5.1 Close Request Packet + +**Direction:** Client → Server +**Flags:** Close (0x04) set. Reply (0x02) clear. HMAC (0x08) if applicable. + +**Header fields present:** +- Magic (3 bytes) +- Flags (1 byte) +- HMAC (16 bytes) — if applicable +- Connection Token (8 bytes) + +No sequence number, no payload. + +### 8.6 Parameter Serialization Format + +Parameters are serialized as a sequence of tag-value pairs in the packet +payload during the open request and open reply. + +Each parameter is encoded as: +1. **Tag:** Unsigned varint (LEB128 encoding). +2. **Value:** Signed varint (zigzag + LEB128 encoding), EXCEPT for + string-typed values. + +For string-typed values, the value is: +1. Length as unsigned varint. +2. Raw UTF-8 bytes. + +**Varint encoding:** This uses the same variable-length integer encoding as +Google Protocol Buffers: +- **Unsigned varint (uvarint):** Standard unsigned LEB128. Each byte uses + the high bit as a continuation flag. The remaining 7 bits contribute to + the value, least significant group first. +- **Signed varint (varint):** Uses zigzag encoding: `(v << 1) ^ (v >> 63)` + for encoding, decoded as `(uv >> 1) ^ -(uv & 1)`. This maps signed + integers to unsigned integers so that small-magnitude values (positive and + negative) have compact encodings. + +See Section 19.4 for cross-language verification notes. + +**Parameter tags:** + +| Tag | Name | Value Type | Unit / Encoding | Description | +|-----|------|-----------|-----------------|-------------| +| 1 | ProtocolVersion | Signed varint | Integer | Protocol version (currently 1) | +| 2 | Duration | Signed varint | Nanoseconds | Test duration | +| 3 | Interval | Signed varint | Nanoseconds | Send interval | +| 4 | Length | Signed varint | Bytes | Packet length | +| 5 | ReceivedStats | Signed varint | Enum (see below) | Server received packet statistics mode | +| 6 | StampAt | Signed varint | Enum (see below) | Server timestamp mode | +| 7 | Clock | Signed varint | Enum (see below) | Clock selection for timestamps | +| 8 | DSCP | Signed varint | Integer (TOS byte value) | IP TOS/Traffic Class byte value (see Section 10.8) | +| 9 | ServerFill | String (length-prefixed) | UTF-8, max 32 bytes | Server payload fill mode | + +Parameters with a zero value MAY be omitted from serialization (they will +default to zero on the receiving end). + +Unknown parameter tags MUST be silently ignored by the receiver. + +The maximum serialized parameter buffer size is believed to be 128 bytes +(see Section 19.10 for verification). + +**ReceivedStats enum values:** + +| Value | Name | Meaning | +|-------|------|---------| +| 0 | None | No received packet statistics | +| 1 | Count | Include received packet count | +| 2 | Window | Include 64-bit received window | +| 3 | Both | Include both count and window | + +**StampAt enum values:** + +| Value | Name | Meaning | +|-------|------|---------| +| 0 | None | No server timestamps | +| 1 | Send | Timestamp at server send | +| 2 | Receive | Timestamp at server receive | +| 3 | Both | Timestamps at both server receive and send | +| 4 | Midpoint | Midpoint timestamp (average of receive and send) | + +**Clock enum values:** + +| Value | Name | Meaning | +|-------|------|---------| +| 1 | Wall | Wall clock only | +| 2 | Monotonic | Monotonic clock only | +| 3 | Both | Both wall and monotonic clocks | + +--- + +## 9. Versioning and Compatibility + +### 9.1 Protocol Version + +The protocol version is negotiated during the open phase. The current (and +only) protocol version is **1**. + +A conforming client MUST set `ProtocolVersion` to 1 in its open request. + +A conforming client MUST verify that the server's returned protocol version +matches exactly. If there is a mismatch, the client MUST abort the session. +The server will also set the Close flag if there is a version mismatch. + +### 9.2 Unknown Fields and Extensions + +Unknown parameter tags in the negotiation payload MUST be silently ignored. + +Reserved flag bits (4-7) MUST NOT be set. Packets with unknown flag bits set +MUST be rejected. + +### 9.3 Server Versions + +This specification targets interoperability with servers implementing +protocol version 1, as found in IRTT versions 0.9.0 and later. Earlier +development versions (0.1.x) used an incompatible protocol and are not +supported. + +--- + +## 10. Client Configuration Parameters + +The following configuration parameters affect protocol behavior: + +### 10.1 Server Address + +- **Effect:** Determines the destination for all packets. +- **Default port:** 2112. +- **Valid values:** Hostname, IPv4 address, IPv6 address (in brackets), + optionally with port. + +### 10.2 Duration + +- **Effect:** How long the client sends test packets. +- **Default:** 60 seconds (1 minute). +- **Valid values:** Positive duration. +- **Protocol:** Sent as nanoseconds in the Duration parameter. The server + MAY reduce this value. + +### 10.3 Interval + +- **Effect:** Time between successive test packets. +- **Default:** 1 second. +- **Valid values:** Positive duration. +- **Protocol:** Sent as nanoseconds in the Interval parameter. The server + MAY increase this value (enforce minimum interval). The server MAY also + decrease it to stay within its timeout constraints. + +### 10.4 Length + +- **Effect:** Total UDP payload length of each test packet. +- **Default:** 0, which means the packet is the minimum size needed for the + header fields. +- **Valid values:** 0 or positive integer. The actual minimum is determined + by the header fields present. +- **Protocol:** Sent in the Length parameter. The server MAY reduce this. + +### 10.5 Received Stats + +- **Effect:** What statistics the server includes about received packets. +- **Default:** Both (count + window). +- **Valid values:** None (0), Count (1), Window (2), Both (3). + +### 10.6 Timestamp Mode (StampAt) + +- **Effect:** When the server records timestamps. +- **Default:** Both (send + receive). +- **Valid values:** None (0), Send (1), Receive (2), Both (3), Midpoint (4). +- **Note:** The server MAY restrict timestamps (e.g., allow only single or + no timestamps). + +### 10.7 Clock Selection + +- **Effect:** Which clock(s) are used for server timestamps. +- **Default:** Both (wall + monotonic). +- **Valid values:** Wall (1), Monotonic (2), Both (3). + +### 10.8 DSCP + +- **Effect:** Sets the TOS/Traffic Class byte in the IP header for both + client and server packets during the active test phase. +- **Default:** 0 (best effort). +- **Valid values:** 0-255 (full TOS byte value). The DSCP field occupies + the upper 6 bits; the lower 2 bits (ECN) are typically zero. + Common values: 0 (best effort), 0xb8 (EF), 0xa0 (CS5), 0x20 (CS1). + [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding B.] +- **Protocol:** The server echoes packets with the negotiated DSCP value. + The server MAY disallow DSCP and reset it to 0. +- **Observed behavior:** DSCP/TOS is applied only to echo request and echo + reply packets. Open and close packets use TOS=0 regardless of the + negotiated DSCP value. [**Verified 2026-04-28** — Finding C.] + +### 10.9 Open Timeouts + +- **Effect:** Controls retransmission during the open phase. +- **Default:** 1s, 2s, 4s, 8s. +- **Valid values:** List of positive durations, each >= 200ms. + +### 10.10 Wait (Drain Period) + +- **Effect:** How long to wait after the last send for outstanding replies. +- **Default:** 3 times the maximum observed RTT, falling back to 4 seconds + if no RTT has been measured. +- **Variants:** + - A factor of maximum observed RTT, with a fallback duration if no RTT + is available. + - A factor of mean observed RTT, with a fallback duration if no RTT is + available. + - A fixed duration. + +### 10.11 HMAC Key + +- **Effect:** When set, an MD5-based HMAC is computed over each packet and + included in the HMAC field. The server MUST also have the same key. +- **Default:** None (no HMAC). +- **Protocol:** When an HMAC key is configured, the HMAC flag (0x08) is set + in every packet. + +### 10.12 Server Fill + +- **Effect:** Requests the server to fill reply payloads with specific data. +- **Default:** Not specified (server uses its default fill). +- **Valid values:** String, max 32 characters. Common values: "none", "rand", + "pattern:XXXX" (hex pattern). +- **Protocol:** The server MAY restrict this to allowed fill types. + +### 10.13 No-Test Mode + +- **Effect:** Opens a connection and immediately closes it without running a + test. Useful for parameter validation. +- **Protocol:** Both the Open and Close flags are set in the open request. + +### 10.14 Loose Mode + +- **Effect:** When enabled, the client accepts server-restricted parameters + with a warning instead of aborting. +- **Default:** Disabled (strict mode). + +--- + +## 11. Timing Semantics + +### 11.1 Timestamp Capture Points + +- **Client send timestamp:** SHOULD be captured as close as possible to the + send system call. For best RTT accuracy, the timestamp should be taken + immediately after the send call returns. +- **Client receive timestamp:** SHOULD be captured as close as possible to + the receive system call return, before any packet processing. +- **Server timestamps:** Determined by the negotiated StampAt parameter: + - Receive: captured when the server receives the packet. + - Send: captured just before the server sends the reply. + - Midpoint: the average of receive and send times. + +### 11.2 Clock Types + +A conforming client MUST track both wall clock and monotonic clock times +for client-side timestamps. The monotonic clock SHOULD be used for all +duration calculations (RTT, IPDV) because it is immune to system time +adjustments. The wall clock is needed for one-way delay calculations. + +### 11.3 Send Scheduling + +The client sends packets on a **fixed isochronous schedule**: + +1. Record the start time. +2. The ideal send time for packet N is: `start + N * interval`. +3. The client SHOULD send each packet as close to its ideal send time as + the system timer allows. +4. The client SHOULD compensate for cumulative timer drift so that packets + remain aligned to integer multiples of the interval from the start time. + The specific drift-compensation strategy is implementation-defined. +5. Before each send, the client checks whether the next scheduled time + would be at or past `start + duration`. If so, sending stops. + +The observable effect is that packets are sent at approximately +`start + 0`, `start + interval`, `start + 2*interval`, etc., with the +last packet sent strictly before `start + duration`. + +### 11.4 Duration and Count Interaction + +- The expected packet count is `ceil(duration / interval)`. + [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding A.] +- The duration is **exclusive**: the last packet is sent at approximately + `start + (ceil(duration / interval) - 1) * interval`, which is strictly + before `start + duration`. +- Example: duration=1s, interval=200ms → packets at 0ms, 200ms, 400ms, + 600ms, 800ms = 5 packets = ceil(1000/200). +- Example: duration=1s, interval=1s → packet at 0ms only = 1 packet = + ceil(1000/1000). The packet at 1000ms is NOT sent (exclusive end). + +### 11.5 Timer Compensation + +The client SHOULD use timer error compensation to improve send accuracy. +The specific compensation algorithm is implementation-defined. Any approach +that produces accurate isochronous sending is acceptable. + +### 11.6 Drain Period + +After the last packet is sent, if not all replies have been received, the +client MUST wait for outstanding replies for the configured wait duration +before closing the session. + +--- + +## 12. Measurement Semantics + +### 12.1 Round-Trip Time (RTT) + +RTT MUST be calculated using client-side monotonic clock values: + +``` +RTT = client_receive_mono - client_send_mono +``` + +If both server send and receive timestamps are available (monotonic +preferred, wall clock as fallback), the server processing time SHOULD be +subtracted: + +``` +server_processing_time = server_send - server_receive +RTT = (client_receive_mono - client_send_mono) - server_processing_time +``` + +This subtraction SHOULD only be applied when both server timestamps are +available. See Section 19.12 for edge-case behavior when server processing +time appears to exceed the raw round-trip time. + +### 12.2 One-Way Delay + +One-way delay requires wall clock timestamps and externally synchronized +clocks (e.g., NTP, PTP). Values are only meaningful when clocks are +synchronized. + +``` +send_delay = server_best_receive_wall - client_send_wall +receive_delay = client_receive_wall - server_best_send_wall +``` + +Where "best receive" prefers the actual receive timestamp, falling back to +the send timestamp if receive is not available (and vice versa for "best +send"). For midpoint timestamps, both best_receive and best_send are the +midpoint value. + +### 12.3 Packet Loss + +``` +packet_loss_percent = 100 * (packets_sent - unique_replies_received) / packets_sent +``` + +If the received count is available from the server: + +``` +upstream_loss_percent = 100 * (packets_sent - server_packets_received) / packets_sent +downstream_loss_percent = 100 * (server_packets_received - unique_replies_received) / server_packets_received +``` + +### 12.4 Duplicate Packets + +A duplicate is a reply with a sequence number for which a reply has already +been recorded. Duplicates MUST be counted but MUST NOT update the original +round-trip data for that sequence number. + +``` +duplicate_percent = 100 * duplicates / total_packets_received +``` + +### 12.5 Late Packets (Out-of-Order) + +A late packet is one whose sequence number is lower than the most recently +received sequence number. + +``` +late_packets_percent = 100 * late_packets / total_packets_received +``` + +### 12.6 IPDV (Jitter) + +IPDV is the difference in delay between two successive successfully received +replies. It is only calculable for sequence number N when both packet N and +packet N-1 have received replies. + +``` +round_trip_ipdv[N] = RTT[N] - RTT[N-1] +send_ipdv[N] = send_delay[N] - send_delay[N-1] (if timestamps available) +receive_ipdv[N] = receive_delay[N] - receive_delay[N-1] (if timestamps available) +``` + +For send and receive IPDV, monotonic clock differences are RECOMMENDED +over wall clock differences when both are available, as the monotonic clock +is not subject to NTP adjustments. Note that IPDV does NOT require +synchronized clocks — only consistent clocks within each endpoint. + +### 12.7 Server Processing Time + +When both server send and receive timestamps are available: + +``` +server_processing_time = server_send - server_receive +``` + +Prefer monotonic clock values if available; fall back to wall clock. + +### 12.8 Per-Packet Upstream/Downstream Loss (Received Window) + +When the 64-bit received window is present in a reply: +- Bit 0 (LSB) represents the current packet (the one being replied to). +- Bit 1 represents the packet with sequence number one less than current, etc. +- A set bit means the server received that packet; a clear bit means it + did not. + +The window may not be valid for all replies. Bit 0 (LSB) is always set +for valid windows, since it represents the current packet which was +received. [**Verified 2026-04-28** — see Section 19.7.] Implementations +SHOULD treat a window value of 0 as potentially invalid and avoid using +it for loss classification. + +The client processes the received window to classify previous packets: +- If a packet was not received by the client but the window says the server + received it → lost downstream. +- If a packet was not received by the client and the window says the server + did not receive it → lost upstream. +- If the window cannot confirm (e.g., more than 64 packets ago, or window + was invalid) → generic loss. + +The window has a maximum lookback of 63 packets (bits 1-63). + +### 12.9 Statistics + +For RTT, send delay, receive delay, and IPDV, a conforming client SHOULD +calculate: +- Minimum, Maximum, Mean, Median, Standard Deviation, Variance. +- Total count (N). + +Median requires storing all values. The method of computing running +statistics is implementation-defined. + +--- + +## 13. Validation Rules + +### 13.1 Magic Bytes + +Every received packet MUST begin with the magic bytes `0x14 0xA7 0x5B`. +Packets with incorrect magic MUST be discarded. + +### 13.2 Flag Validation + +Reserved flag bits (4-7) MUST be zero. Packets with invalid flag bits MUST +be discarded. + +### 13.3 Reply Flag + +All packets received by the client MUST have the Reply flag (0x02) set. +Packets without the Reply flag MUST be discarded. + +### 13.4 HMAC Validation + +If an HMAC key is configured: +- Received packets MUST have the HMAC flag set. +- The HMAC MUST be validated: + 1. Extract the HMAC value from the packet. + 2. Zero the HMAC field in the packet buffer. + 3. Compute HMAC-MD5 over the entire packet buffer. + 4. Compare the computed HMAC with the extracted value using a + constant-time comparison. +- Packets with missing or invalid HMAC MUST be discarded. + +If no HMAC key is configured: +- Received packets MUST NOT have the HMAC flag set. Packets with an + unexpected HMAC MUST be discarded. + +### 13.5 Sequence Number Validation + +Echo reply sequence numbers MUST correspond to a previously sent packet. +Replies with sequence numbers outside the range [0, packets_sent) MUST be +rejected. + +### 13.6 Timestamp Consistency + +If both receive and send timestamps are present in a reply, they MUST use +the same clock mode (both wall, both monotonic, or both sets). Inconsistent +clock modes MUST cause the packet to be rejected. + +Midpoint timestamps MUST be exclusive — if a midpoint timestamp is present, +no receive or send timestamp fields may also be present. + +### 13.7 Close Flag in Echo Reply + +If the server sets the Close flag in an echo reply, the client SHOULD +process the reply normally (record measurements) and then close the +connection. This is defensive handling for the possibility that the +server can forcibly end a session mid-test. + +**Note:** This behavior has not been directly verified by black-box +testing (see Section 19.3). Implementations SHOULD handle it defensively. + +--- + +## 14. Error Handling + +| Error Category | Client Behavior | +|---------------|----------------| +| Name resolution failure | MUST abort. | +| Socket creation / bind failure | MUST abort. | +| Server unreachable (all open timeouts exhausted) | MUST abort. No partial results. | +| Protocol version mismatch | MUST abort. | +| Server restriction rejected (strict mode) | MUST abort. | +| Server restriction accepted (loose mode) | SHOULD warn, continue. | +| DSCP not supported by OS | MUST abort if DSCP was requested non-zero. | +| DF bit not supported by OS | MUST abort if DF was requested non-default. | +| Open reply with Close flag (not no-test) | MUST abort (server rejected session). | +| Zero connection token (without close flag) | MUST abort. | +| Send error during test | SHOULD abort, report partial results. | +| Receive error during test | SHOULD abort, report partial results. | +| Individual malformed packet during test | SHOULD abort. | +| Short reply (shorter than negotiated length) | SHOULD abort. | +| Stamp-at mismatch in reply | SHOULD abort. | +| Clock mismatch in reply | SHOULD abort. | +| Unexpected sequence number | SHOULD abort. | +| Context cancellation (user interrupt) | SHOULD stop gracefully, close connection, report partial results. | +| Insufficient resources for test parameters | MUST abort. | + +--- + +## 15. Result Model + +After a test, the client implementation SHOULD expose the following +information: + +### 15.1 Per-Packet Data (Round Trips) + +For each sent packet (indexed by sequence number): +- Sequence number. +- Lost status: not lost, lost (generic), lost upstream, lost downstream. +- Client send timestamp (wall + monotonic). +- Client receive timestamp (wall + monotonic) — empty if lost. +- Server receive timestamp (wall and/or monotonic) — as negotiated. +- Server send timestamp (wall and/or monotonic) — as negotiated. +- Computed RTT. +- Computed send delay. +- Computed receive delay. +- Computed IPDV (round-trip, send, receive) relative to previous packet. + +### 15.2 Aggregate Statistics + +- Test start time (wall + monotonic). +- Actual test duration. +- Packets sent. +- Packets received (unique). +- Packet loss percentage. +- Upstream loss percentage (if server count available). +- Downstream loss percentage (if server count available). +- Duplicate count and percentage. +- Late packet count and percentage. +- RTT statistics: min, max, mean, median, stddev, variance. +- Send delay statistics (if timestamps available). +- Receive delay statistics (if timestamps available). +- IPDV statistics (round-trip, send, receive). +- Server processing time statistics. +- Bytes sent and received. +- Send and receive bitrate. +- Timer error statistics. +- Timer miss count and percentage. +- Wait duration actually used. + +### 15.3 Configuration + +- Local and remote addresses (resolved). +- Negotiated parameters. +- Originally supplied parameters. +- IP version used. + +### 15.4 Errors + +- Send error (if the send operation failed). +- Receive error (if the receive operation failed). + +--- + +## 16. Interoperability Requirements + +1. A conforming client MUST be able to open a session with a compatible IRTT + server running protocol version 1. + +2. A conforming client MUST send open requests with correctly serialized + parameters using the varint-encoded tag-value format. + +3. A conforming client MUST correctly parse server open replies, including + restricted parameters. + +4. A conforming client MUST include the connection token in all post-open + packets. + +5. A conforming client MUST send test packets in the negotiated format, with + the correct field layout and packet length. + +6. A conforming client MUST correctly match replies to sent packets using + sequence numbers. + +7. A conforming client MUST tolerate UDP reordering, duplication, and loss. + +8. A conforming client MUST validate magic bytes, flags, and HMAC on all + received packets. + +9. A conforming client SHOULD produce RTT measurements consistent with + upstream implementations within the precision of the system clocks. + +10. A conforming client MUST send a close packet at the end of the session. + +11. A conforming client MUST correctly handle the HMAC calculation: + compute HMAC-MD5 over the full packet with the HMAC field zeroed. + +12. When computing an outgoing HMAC, the client MUST: + a. Set the HMAC flag bit. + b. Zero the HMAC field. + c. Compute HMAC-MD5 over the entire packet buffer. + d. Write the resulting MAC into the HMAC field. + +--- + +## 17. Black-Box Test Plan + +### Test 1: Basic Connectivity + +- **Setup:** Start an upstream IRTT server with default settings. +- **Client config:** Default parameters (1s interval, 1m duration), reduce + to 5s duration for testing. Server address: localhost. +- **Expected:** Session opens, packets are exchanged, results include RTT + measurements, session closes cleanly. +- **Pass criteria:** Non-zero packets_sent and packets_received. RTT values + present. No errors. + +### Test 2: Parameter Negotiation + +- **Setup:** Start a server with `--max-length=100`. +- **Client config:** Request length 200. +- **Expected (strict mode):** Client aborts with server restriction error. +- **Expected (loose mode):** Client continues with length 100. +- **Pass criteria (strict):** Client exits with error indicating server + reduced length. +- **Pass criteria (loose):** Successful test with negotiated length 100. + +### Test 3: HMAC Authentication + +- **Setup:** Start server with `--hmac=testkey`. +- **Client config:** `--hmac=testkey`. +- **Expected:** Session succeeds. +- **Pass criteria:** Successful test with no errors. +- **Variant:** Client uses wrong key. Expected: open timeout (server drops + packets with bad HMAC silently). + +### Test 4: Packet Capture Verification + +- **Setup:** Start server on localhost. Capture with tcpdump on the loopback + interface, port 2112. +- **Client config:** Short test (5s, 1s interval). +- **Expected:** Capture shows: + - Open request: magic bytes 14 a7 5b, flags=0x01, followed by varint + params in payload. + - Open reply: magic bytes, flags=0x03 (open+reply), 8-byte token, params. + - Echo requests: magic bytes, flags=0x00, 8-byte token, 4-byte sequence number. + - Echo replies: magic bytes, flags=0x02 (reply), 8-byte token, 4-byte + sequence number, optional stats and timestamps. + - Close request: magic bytes, flags=0x04. +- **Pass criteria:** All packets have correct magic, flags, and field layout. + +### Test 5: Sequence Number Handling + +- **Setup:** Server with default settings. +- **Client config:** 10s duration, 200ms interval (expect ~51 packets). +- **Expected:** Reply sequence numbers are in range [0, packets_sent). + Results show correct per-packet data. +- **Pass criteria:** All received replies have valid sequence numbers. No + unexpected sequence number errors. + +### Test 6: Timestamp Modes + +- **Setup:** Server with default settings. +- **Variants:** + - `--tstamp=none --clock=wall` + - `--tstamp=send --clock=wall` + - `--tstamp=receive --clock=monotonic` + - `--tstamp=both --clock=both` + - `--tstamp=midpoint --clock=both` +- **Expected:** Each variant produces the correct timestamp fields in replies. + RTT is always available. One-way delay is only available with wall clock + timestamps. +- **Pass criteria:** Results match expected availability of delay measurements. + +### Test 7: Loss Detection + +- **Setup:** Use a network impairment tool (e.g., `tc netem loss 10%`) or a + server on a lossy link. +- **Client config:** 30s duration, 100ms interval. +- **Expected:** packet_loss_percent is non-zero. With `--stats=both`, + upstream and downstream loss are differentiated. +- **Pass criteria:** Loss percentages are reported. With received window, + per-packet loss direction is classified. + +### Test 8: IPv6 + +- **Setup:** Server listening on IPv6 (::1). +- **Client config:** `-6 ::1`. +- **Expected:** Successful test over IPv6. +- **Pass criteria:** IP version in results is IPv6. + +### Test 9: No-Test Mode + +- **Setup:** Server with default settings. +- **Client config:** `-n localhost`. +- **Expected:** Connection opens, parameters are negotiated, connection + closes immediately. No test packets sent. +- **Pass criteria:** No round-trip data. No errors. + +### Test 10: Large Packet Size + +- **Setup:** Server with default settings. +- **Client config:** `-l 1472 -d 5s localhost`. +- **Expected:** Successful test with 1472-byte packets. +- **Pass criteria:** Non-zero results. + +### Test 11: Server Fill + +- **Setup:** Server with `--allow-fills=rand`. +- **Client config:** `--sfill=rand -l 172 -d 5s localhost`. +- **Expected:** Successful test. Server fills reply payloads with random data. +- **Pass criteria:** Test completes without error. + +### Test 12: DSCP + +- **Setup:** Server with default settings (DSCP allowed). +- **Client config:** `--dscp=46 -d 5s localhost`. +- **Expected:** Packets are sent with EF DSCP marking. Capture verification + shows TOS/TC field set. +- **Pass criteria:** Test completes. Packet capture confirms DSCP value. + +--- + +## 18. Packet Test Vectors + +### 18.1 Echo Request with HMAC + +**Scenario:** Echo request with HMAC, received stats (both), timestamps +(both, both clocks), pattern fill `0xFF 0xFE 0xFD 0xFC`, 92-byte packet +(76-byte header + 16-byte payload). +[**Size corrected 2026-04-28** — originally described as 256 bytes, but +HMAC verification confirms the test vector is 92 bytes. See +BLACKBOX_VERIFICATION_REPORT.md Finding E.] + +**Connection token:** `0x886bc9a722b33eea` (little-endian) +**Sequence number:** `0x6fe2a1bb` (little-endian) +**HMAC key:** `0x3c 0x68 0x1d 0x39 0x41 0x1d 0x72 0x43` + +**Fields present (in order):** +1. Magic: 3 bytes +2. Flags: 1 byte (0x08 = HMAC) +3. HMAC: 16 bytes +4. Connection Token: 8 bytes +5. Sequence Number: 4 bytes +6. Received Count: 4 bytes (zeroed) +7. Received Window: 8 bytes (zeroed) +8. Receive Wall: 8 bytes (zeroed) +9. Receive Mono: 8 bytes (zeroed) +10. Send Wall: 8 bytes (zeroed) +11. Send Mono: 8 bytes (zeroed) + +**Header total:** 3+1+16+8+4+4+8+8+8+8+8 = 76 bytes +**Payload:** 92 - 76 = 16 bytes of repeating pattern + +**Hex (92 bytes):** +``` +14 a7 5b 08 e7 03 41 e7 d4 08 cf 69 41 f3 f4 78 +5a 56 0c 4c ea 3e b3 22 a7 c9 6b 88 bb a1 e2 6f +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 ff fe fd fc +ff fe fd fc ff fe fd fc ff fe fd fc +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `08` (HMAC set) +- Bytes 4-19: HMAC `e7 03 41 e7 d4 08 cf 69 41 f3 f4 78 5a 56 0c 4c` +- Bytes 20-27: Connection Token `ea 3e b3 22 a7 c9 6b 88` (LE: 0x886bc9a722b33eea) +- Bytes 28-31: Sequence Number `bb a1 e2 6f` (LE: 0x6fe2a1bb) +- Bytes 32-35: Received Count (zeroed) +- Bytes 36-43: Received Window (zeroed) +- Bytes 44-51: Receive Wall Timestamp (zeroed) +- Bytes 52-59: Receive Mono Timestamp (zeroed) +- Bytes 60-67: Send Wall Timestamp (zeroed) +- Bytes 68-75: Send Mono Timestamp (zeroed) +- Bytes 76+: Payload (pattern fill) + +**Client behavior:** This packet is sent to the server during the active test phase. + +### 18.2 Echo Reply with HMAC and Full Timestamps + +**Scenario:** Echo reply with HMAC, received stats (both), timestamps (both, +both clocks), pattern fill, 92-byte packet (76-byte header + 16-byte payload). +[**Size corrected 2026-04-28** — see Finding E.] + +**Connection token:** `0xe666ceb6766fcbc3` (little-endian) +**Sequence number:** `0x1d3b0706` (little-endian) +**HMAC key:** `0xda 0xb3 0xe9 0x04 0xa6 0x87 0x92 0x49` +**Received count:** `0xa3bc9f19` (little-endian) +**Received window:** `0xd7e939e586f83b9b` (little-endian) + +**Timestamps (little-endian int64):** +- Receive wall: `0x525af13dee2a75a1` +- Receive mono: `0x2d5562223e4ac69a` +- Send wall: `0x589705d446293f69` +- Send mono: `0x461df12fdd2c5066` + +**Fields present (in order):** +1. Magic: 3 bytes +2. Flags: 1 byte (0x08 = HMAC, plus 0x02 = Reply → but let's verify from the test vector) +3. HMAC: 16 bytes +4. Connection Token: 8 bytes +5. Sequence Number: 4 bytes +6. Received Count: 4 bytes +7. Received Window: 8 bytes +8. Receive Wall: 8 bytes +9. Receive Mono: 8 bytes +10. Send Wall: 8 bytes +11. Send Mono: 8 bytes + +**Note on flags:** The flags byte in this test vector is `0x08` (only HMAC +set). This vector is provided for verifying field layout and HMAC +computation only. **In actual protocol operation, the Reply flag (0x02) +MUST also be set on all server-to-client packets**, making the expected +flags byte `0x0A` for a normal HMAC echo reply. Implementers MUST +set the Reply flag in actual protocol exchanges; the HMAC computation +itself does not depend on the Reply flag's value. + +**Hex (first 92 bytes):** +``` +14 a7 5b 08 d2 98 a3 4a 6a 13 41 02 68 b2 67 a8 +d6 7e 28 25 c3 cb 6f 76 b6 ce 66 e6 06 07 3b 1d +19 9f bc a3 9b 3b f8 86 e5 39 e9 d7 a1 75 2a ee +3d f1 5a 52 9a c6 4a 3e 22 62 55 2d 69 3f 29 46 +d4 05 97 58 66 50 2c dd 2f f1 1d 46 ff fe fd fc +ff fe fd fc ff fe fd fc ff fe fd fc +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `08` (HMAC; see note above about Reply flag) +- Bytes 4-19: HMAC `d2 98 a3 4a 6a 13 41 02 68 b2 67 a8 d6 7e 28 25` +- Bytes 20-27: Connection Token `c3 cb 6f 76 b6 ce 66 e6` (LE: 0xe666ceb6766fcbc3) +- Bytes 28-31: Sequence Number `06 07 3b 1d` (LE: 0x1d3b0706) +- Bytes 32-35: Received Count `19 9f bc a3` (LE: 0xa3bc9f19) +- Bytes 36-43: Received Window `9b 3b f8 86 e5 39 e9 d7` (LE: 0xd7e939e586f83b9b) +- Bytes 44-51: Receive Wall `a1 75 2a ee 3d f1 5a 52` (LE: 0x525af13dee2a75a1) +- Bytes 52-59: Receive Mono `9a c6 4a 3e 22 62 55 2d` (LE: 0x2d5562223e4ac69a) +- Bytes 60-67: Send Wall `69 3f 29 46 d4 05 97 58` (LE: 0x589705d446293f69) +- Bytes 68-75: Send Mono `66 50 2c dd 2f f1 1d 46` (LE: 0x461df12fdd2c5066) +- Bytes 76+: Payload (pattern fill) + +**Expected client behavior:** Validate magic, validate HMAC, extract +connection token, extract sequence number, extract received stats, extract timestamps, +record round-trip data. + +--- + +## 19. Open Questions / Verification Required + +### 19.1 Open Request Field Layout — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** The open request does NOT include a connection token field. The +layout is: magic (3) + flags (1) + [HMAC (16) if applicable] + params. +Verified by packet capture (`captures/full-session.pcapng`, +`captures/hmac-session.pcapng`). + +### 19.2 Minimum Packet Length Calculation — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** Minimum echo packet lengths verified for all stats/tstamp/clock +combinations. See BLACKBOX_VERIFICATION_REPORT.md Section 19.2 for the +complete table. The minimum is 16 bytes (no stats, no timestamps) and the +maximum default is 76 bytes (both+both+both+HMAC). + +### 19.3 Server Close During Test — OPEN / Verification Required + +**Status:** Not tested. Hard to trigger reliably with black-box testing. + +**Question:** Can the server set the Close flag on an echo reply mid-test +(e.g., due to exceeding a duration limit)? If so, how should the client +respond? + +**Compatibility recommendation:** The client SHOULD check for the Close +flag in echo replies and, if set, process the reply normally (record +measurements) and then terminate the session. This is defensive handling +for a scenario that has not been directly observed in black-box testing. + +### 19.4 Varint Encoding Compatibility — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** Parameters decoded from captured packets match expected values +using standard protobuf-style zigzag + LEB128. All parameter tags and values +verified. See BLACKBOX_VERIFICATION_REPORT.md Section 19.4. + +### 19.5 HMAC Computation Scope — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** HMAC-MD5 is computed over the entire packet buffer (including +payload) with the 16-byte HMAC field zeroed. Independently verified with +Python for all four packet types (open request, echo request, echo reply, +close request). The spec's test vectors (18.1 and 18.2) also verify +correctly at 92-byte packet size. + +### 19.6 Send Timestamp Capture Timing — OPEN (not externally testable) + +This is an implementation-internal detail that cannot be verified via +black-box testing. The client SHOULD capture timestamps as close to the +send/receive system calls as possible (see Section 11.1), but the exact +timing is an implementation choice. + +### 19.7 Server Received Window Validity — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** Bit 0 (LSB) of the received window is always set for valid +windows (it represents the current packet which was received). Observed +window values: seq 0 → 0x01, seq 1 → 0x03, seq 2 → 0x07. A window +value of 0 can be used as the invalid sentinel. See +BLACKBOX_VERIFICATION_REPORT.md Section 19.7. + +### 19.8 Open Reply Without HMAC When Client Expects HMAC — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** The server silently drops packets with missing or invalid HMAC. +The client times out with `[OpenTimeout] no reply from server`. Same +behavior for wrong key and no key. See BLACKBOX_VERIFICATION_REPORT.md +Section 19.8. + +### 19.9 Minimum Restricted Interval Safety Floor — PARTIALLY RESOLVED + +**Status:** Partially tested 2026-04-28. + +**Result:** In strict mode, the client rejects ANY server parameter change +with a non-zero exit code. In loose mode, changes are accepted with a +warning. The exact safety floor for server-decreased intervals could not +be tested without a cooperative malicious server. See +BLACKBOX_VERIFICATION_REPORT.md Section 19.9. + +### 19.10 Maximum Parameter Buffer Size — PARTIALLY RESOLVED + +**Status:** Partially tested 2026-04-28. + +**Result:** Largest observed parameter payload was 52 bytes (all params +including 24-character ServerFill). Upper bound estimate is ~82 bytes. The +128-byte limit appears safe. See BLACKBOX_VERIFICATION_REPORT.md +Section 19.10. + +### 19.11 Field Ordering Verification — ✅ RESOLVED + +**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64. + +**Result:** The field ordering in Section 8.1.3 is correct. Verified with +six different field combinations including no optional fields, various +stats/tstamp combinations, midpoint timestamps, and HMAC. See +BLACKBOX_VERIFICATION_REPORT.md Section 19.11. + +### 19.12 RTT When Server Processing Time Exceeds Raw Round-Trip — OPEN / Verification Required + +**Status:** Not tested. Hard to trigger on localhost. + +**Question:** When server processing time (as reported by server +timestamps) exceeds the client-measured round-trip time — possible due to +clock granularity or drift — the subtraction would produce a negative RTT. +Should the client skip the subtraction, clamp to zero, or report the +negative value? + +This edge case requires server processing time to exceed the raw +client-measured RTT, which is extremely unlikely under normal conditions. + +--- + +## 20. Non-Goals + +This specification does **not** require: + +- Matching upstream source layout, file organization, or module structure. +- Matching upstream private names (functions, types, variables). +- Matching upstream internal algorithms where equivalent behavior can be + achieved independently. +- Matching exact CLI text output. +- Implementing the server. +- Implementing every optional upstream feature in the first milestone. +- Implementing timer compensation with any specific algorithm (only accurate + send timing is required). +- Implementing any specific concurrency model. +- Using any specific programming language features or libraries. +- Supporting IRTT protocol versions other than 1. +- Supporting the development-era (0.1.x) protocol. diff --git a/docs/protocol/captures/basic-session.pcapng b/docs/protocol/captures/basic-session.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..ead1d084d516ffbd61fd8d94063e2a1e368209b5 GIT binary patch literal 1444 zcmb7^PiPZC6vkhZP2E)M-wLG)4uUi>H8k68Qc*N&M3j^kU8oul-Rwr(Cfi-MiMF-q zK|JInh(<4-)dY@AtkXZ#p_VJMIC% z@Znfb8(n|95daa6*>HCea8K9!u#I~F=krD|BU{j0zA_jR_Zw>{G z8>8)zyO32b(LH8>=1G9_A9ezyw*lOwZDKBm#582q$GRTpKz{x7%d@o|-PnICfy{r| z`HOyb>baLKrk6u5JMcFfdkwk`OwS!XGh^1fS0{wVC;j<~>1ds49{Y4H=kslR^Rjuq zk=sh}*h?Jt!dEYw=ey~7gWG_(`2IHfKJcizh>xDUWBTDd(>(T_l?k8kmqjm|=ezl> z|JOT@J-+H^*Kc{*Jm1I`Xl`-gTeIVD(f6Ujec$oXXp!lKvrO~YAA4qfzH7Z+HqSS5 zTL~UJw#(1{eB8_C`9^k#<{r0!^nG}7xPRuF|gM5T2*O--EhLHOqL7I6tNxas-YKjlAj?u hyA1J8#k010M8UfZ_n+=p6@l)j#dy zJG~Vqy1zgfyA)w~C3+>O;DvvHyA;H0h3a|I78XA)_P~>zlcdk@yy=^PWO87Qh~`g@ z9ggt%S5FFQbjI^)RWWZwX^iT2<>G=ca{8p685iZ2tU1EST1Lhwh?Hgfw`GHSzB`eo z7)_GZs(TgN6RDE)tILbFzmyiHp3_Z{D$1s)xb-@pZssedoi_`4)0s*$od~mFjyq@q zX{1n;YVHIdHJWo>uWVPA_V80G-``97h+2*(JlAguBa@jT8r$ELkadj>A?wF}Lr!xI z_p)-Eo8heOEN30-WH~DmQIyr^pSKdx;22j`HKA%8h)iTVYzWz}4jA$**KjX;=Emi4 zwpQHE(rD)tXGcpj$@1*_o7ewiARCMoHU!4XkRic_1BOmls^7zmV1BDpVg~gY`2B7K z=AZ13|6srT5M~GK>#~)dZ0sc%T0Qvq>%02Kp$m-K#%{yzxoesI+N zYY$kD!t7w(y*3%Ty?(pnJ Y{M#b-JLKSXR!-S(cnO>rA&T3zu7YJ*(xjg?!eSN+BJ9&O+z7 zM}-#>2oQyuJ&vCelx*9nYWn;h+$G}my)XpOFdXLCZk;KaOb%e*F3(FC;*sAR6&MdL zi(sCw@DrGm1k^cz{F7~fGy4G^;xS3>4M~Ngo!s!tn~{hhEN{O4J``Pkwe>bWAc|sK zl)ed`CwUtv3i8&bE4+v~Deyl1>r2>MQrjDl?wmsJXriD_9#|Uv@h=8>6JrTQL5w4% z3a5TZ9^(yM|Afu-oL}vfkpW!>-M^b4=1%rXAGY{2%=Y^8*}~s!nm>u6@M8VzJF9hY z4QM7s70BZaez!GKup)(T~0LxR-r#GtBPSH?e6((E$AM z+T5}C*!TL(^BeTlTFbBRuljYDT?+QoykXVkV_1`d-&As{9A9q%TvTxD7l@uS56CnT$$>NXo|!xU-#vH!jBjYjR|kNF z>9M0O9RFD11|BY|YBA3iq8=CnSMV4`Rn9Rk+WPCCim!^V92cW&s zJ|0tH1P^ZTKv4- zo0_C8VN#Ixt$9I5DWJ5pXFiBdZ9d&_ZdN_ zekctF56fR?oD5_CsrC{wz-G|<{RA=hvrjK}vwz^C&a*T6GuiG=Hhq6eBUU$WhfCWd zUlO0DseZj~*5xZ^ZL{ai)kfcemApQVHKld7!sn+>w$b-+`waH&wb`UWeb9MP&>6Pw z%UvE=eBspE+xm|OW_x?!*NWWb0W|c!Guqp-J@CC*)9ddxGCe(jj9wmi`#^SjfWfl# z(aA9OZ+XCGQ2+ge{T*neptGA2yV=o~PBx#${wxo4vO7J{yf&AMpa)L)>(}Z2h-lWa zQ)X?mJ3Zi6w!1vAd)~=5`nEh^vq@tBvxrGSeZ2a;qr2cZ%qi7UC9jp4sws~OQeI-& j4JMgCI{IJIS}kx=r~eeq1Nfd7=RQ*kb$M%M4#fBisrU-B literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/full-session.pcapng b/docs/protocol/captures/full-session.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..d6aa5d4324fddcb98957ada5a1eeca3513febdd3 GIT binary patch literal 1380 zcmb7^&ui0Q7{{NcU7F2qAdVqJ@jbbgjk2cM+TjirD}rrN6v>7?G)+3%^+(dI)lEdJ zpdKA#DtOsNP&Wke;IEVSUG(D7^Yjl8nOJ?ECIw@2un#L6B#(uB&vd`9?Y`3|)GyClZ-i}8DcX947 z$7deUtyad;uVhPi*=c(}opWw&U7kWsX`l1pnA B{qFz( literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/hmac-session.pcapng b/docs/protocol/captures/hmac-session.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..54e0bd1d3526d09ad0a96801505f0e67be547c54 GIT binary patch literal 1244 zcmd<$<>i{fz`)>ZqbV?jYqs)DZ(GlMomZep^3u!519p0S>xjzUst zW=@KNk*SN3p^=4xhD%~md1fAv50o{~WME-1W^gIZEl5r*P|yg^EJ`iTNG!_MR4~yq z(=$=faL+7JNY2mA1yX6|$tH>BCKl!l9W4hAL$W`>-610c!4 z;FF(Ukd&C5%?VV+29)P!;A3DYO({?)$S*2UFfueWVPN1P(Y+vRK=y&m%K&kJpe}%o zVa@Wbu^vD+2r~h(D+7beR&@pj2SEmgdZ05w_~dgACkBIm&LYdBIsZ=(TfQo|LTujQ zMOQ=a7??3KF*RIR^O=viVa2tb?5u2TZ0u|x^Fj6$05J%F?CD>zH8udq24Sc@KE;zz z?cs1b`JBPxpEuAR?hKAPk11bE_RUz|`RDkQ4!Ouv_OY*DwmW094`dI>z8)Y30g!#+ zE4Rkx0NEf6wJ#&j6V*Pjze4_{1MTCu+3w%8Sy;d@a;;8(_O18-VfKN{BL#rmfbNz> zSllwb6V)wXzeC)@_1fS-ZF1>i?#k)1s~*c9yoT%+P`UwO5cuIXk9S_~Dhc5?5pp1Y zLllG`&lCyfV{=R1ha(u_vi1wATad$La@)L(^QPIKeaZ7|<^GK`>kPpJp^kVVz0;*fU;iB@-9GG5szMkb@d2eoQTJOZA zMjm`l4`AT}GLsdEbAT8GKxRjlUx{U8kYRAnFU>1SEh<(hD9SI(Oi4{qNUBswffOR3 ivIwNscjbauP#FMXn*#N*0NLyiqk!geKv@t9WCsA6xgCK3 literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/large-packet.pcapng b/docs/protocol/captures/large-packet.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..02626b61060947f4ed3ae4e9eb55be18fe5a0eb0 GIT binary patch literal 3716 zcmeHKO=}ZT6up_Rj@1f1rz}rUAbKIgcTXguj9I69QFVx3F zD!O5U0xF1b3a=(q!Z513Ui}EZW#IFpa2%k;BNl~m!mu41;Mf8CB^lBqzBMW^o-7-1 z@(dSqm=hGlWq{&m4dCK1z*B6Ku)Sk0!l_PSWb<|=qbTjS-@go~-K*9f)eXZKFuI(? z%@GvD{Smq6F(>r6FI;=o&uuPl??AqD9=X+tXN?z|tGCAX(;#lrNH3|zu*Jns9e|K( z=v(X0`!%S4{j0i#8nCNDG&1A_Ovag#-Hc;DwW j&Z>_XPnSK~8R^~10vEKaQGNw~r(sTf6guX6gEamCHq9G@ literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/long-sfill.pcapng b/docs/protocol/captures/long-sfill.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..9afbefe931e6c4f98f6e0d4c440110f81450027c GIT binary patch literal 3848 zcmeHKPiWI%6n{;cESpX!6ddB=^JL3LY17!wp|G)vf7q%pal!;mlT<^~CSB2JtBo@9%>H05#oVj_p<%mXiV4|A+Mw4e=4) z3?*pyE{Kp{dghH`OwOU70*HMs0Zc{!ma$Co-Rn{wqOGIRmkZ%X-ycuMUObr~48rQ3 zfv`484ev5uBloc`HlO!nt*;=~WNQwwLLrW;zxw!gfN!ti`awYy#Q{_#OnmgFpxVy$+E0lm7S4P`gXpzsyjPX21To=N^BC>gqpX;K%?LIvLF}>7B1$ z2oA%vU8~>#w8$LSzG>=)&I)&!-WfP)Zl!0JcdmLgFH$Vw=fW7b-%W@mVBBlO@dK?q BQfdGI literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/no-test.pcapng b/docs/protocol/captures/no-test.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..53e99994d9e54868e5158278132e5f5dea9fdb4e GIT binary patch literal 568 zcmb7BJx{_=6uq>xRQyOxoQ%VBqro&#%2$GkD8xmgi30;Lloqw2rA?uJ4N8nZ!Nt)@ z2Ny?O++k<-2k2iw?^R56aPTCj_uRbu&bjG}&dx@!0H9KcFNUzbPr!i$MbEQatfKM| z2S+!ycbS?|Qc7}#H3Q2wnVK%CNj1k}CEY)=I@sfBLWVG;pcFWsp?fS=wft81K=<1+ z)0B*&vDl{7V}|QESoX4prf0QWRx|T5rUTF}w8t?upOAn99&9&(UkSEc*K6uVdkUvS zJfDU+fWY*a=lVUSCX*?EnSbs}F_cGsb!rf2J0e`XRvUTLL=IU7NI$Fttjz1(_fMooJW1k^u486PJ4bCv)A literal 0 HcmV?d00001 diff --git a/docs/protocol/captures/tstamp-none.pcapng b/docs/protocol/captures/tstamp-none.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..186ecffdc42848707fdc1f705983215d46791f94 GIT binary patch literal 2160 zcmb7`O-K}R6u_Ue>+EEi85>B63?#Vbw(YK~yP^kUEyJ`H6qM3+zpNHrcU?cy2<;L? zr=%cHAsQ6bB?75S2k{_Eb}~;AL>&VAIB5#5y*D2;|DEyBgE#ZW+4;TqpWn=`dHMNy zj{u;huHtwO?)Q}7K!CHUR9}zS;x<7AB*M|lt)jcyQR#5m#mI2HFDAOH8r&|oPqa0J z(>LS&c#LPAS;xVz?!dct0h z&+CcRlw)@QnuX>Vr2{Dh;J^fZNhfXvXig?mk#My40Dg+c^9SKDz;G-jrjqF)(d}|o z0UY|zd8rNcBVHRFV2lTNs6A2;^k7ZNVV(nUeh~ob3jro@Ov=qA!4JimGuCHUb8#(L&n7-FdEK~4rEpW|vU#@+crWj+LfX6wMDAlCL)YtV-^rNZ(!pRlaOWppEED)nx1Ii9m{ z#3OD9lY+S4{23w|<{B(F%K6F^kW2_&UA3_H4$rT0h$G2N` z{s(GVzBIS&A5^|f__tO4YuSqiY{yrYEzK^oIW3#sxj?sNbUN{(NM&o?MJ$s#cOw^B zwlufmPGuXp`}ku@=WgD?vKD6db!XTaix?KKY|~Q zbvXI7YDUM>Z_e{0C6^-wC$$c2)(26KBdjr1L<@@2yQTk1jgTiYvt zPv7h0X||W($F<14IBhTTsXE;{Y=1a|@8y+lYokf&k?oexKhbT+bCk-uY+D0tFUg1E zR_)b{pU+j8xRbu4y6&Fp&Vgd^Cx`oodeVbpDxDmO$9iI7WK4|7KkW4X0o~6$=S~;> VE>m5G*O{@l5(py~Yi-cI`3uV%oz?&V literal 0 HcmV?d00001 diff --git a/docs/protocol/test-vectors/README.md b/docs/protocol/test-vectors/README.md new file mode 100644 index 0000000..d9f2cbc --- /dev/null +++ b/docs/protocol/test-vectors/README.md @@ -0,0 +1,304 @@ +# IRTT Packet Test Vectors + +**Source:** Black-box captures from irtt 0.9.1 on macOS Darwin 25.3.0 arm64 +**Date:** 2026-04-28 + +All vectors are from actual network captures (not constructed). + +--- + +## Vector 1: Open Request (no HMAC) + +**Scenario:** Client opens session with default parameters (duration=3s, +interval=1s, stats=both, tstamp=both, clock=both). +**Direction:** Client → Server +**irtt version:** 0.9.1 +**Command:** `irtt client -d 3s -i 1s 127.0.0.1:2112` + +**Packet (24 bytes):** +``` +14 a7 5b 01 01 02 02 80 f8 82 ad 16 03 80 a8 d6 +b9 07 05 06 06 06 07 06 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `01` (Open) +- Bytes 4-23: Parameter payload: + - Tag 1 (ProtocolVersion): value `02` → zigzag → 1 + - Tag 2 (Duration): value `80 f8 82 ad 16` → zigzag → 3000000000 ns (3s) + - Tag 3 (Interval): value `80 a8 d6 b9 07` → zigzag → 1000000000 ns (1s) + - Tag 5 (ReceivedStats): value `06` → zigzag → 3 (Both) + - Tag 6 (StampAt): value `06` → zigzag → 3 (Both) + - Tag 7 (Clock): value `06` → zigzag → 3 (Both) + +**Notes:** +- No connection token field (token not yet assigned) +- Tags 4 (Length), 8 (DSCP) omitted because values are 0 +- Tag 9 (ServerFill) omitted because not specified + +--- + +## Vector 2: Open Reply (no HMAC) + +**Scenario:** Server accepts session, returns token and echoed parameters. +**Direction:** Server → Client +**Token:** `0x7896b6ab87715213` + +**Packet (32 bytes):** +``` +14 a7 5b 03 13 52 71 87 ab b6 96 78 01 02 02 80 +f8 82 ad 16 03 80 a8 d6 b9 07 05 06 06 06 07 06 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `03` (Open | Reply) +- Bytes 4-11: Connection token `13 52 71 87 ab b6 96 78` (LE: 0x7896b6ab87715213) +- Bytes 12-31: Parameter payload (same as request — no restrictions) + +--- + +## Vector 3: Echo Request (no HMAC, default stats+timestamps) + +**Scenario:** First test packet, sequence 0, default settings. +**Direction:** Client → Server +**Token:** `0x7896b6ab87715213`, Seq: 0 + +**Packet (60 bytes):** +``` +14 a7 5b 00 13 52 71 87 ab b6 96 78 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `00` (no flags — echo request from client) +- Bytes 4-11: Connection token +- Bytes 12-15: Sequence number `00 00 00 00` (LE: 0) +- Bytes 16-19: Received count (zeroed placeholder) +- Bytes 20-27: Received window (zeroed placeholder) +- Bytes 28-35: Receive wall timestamp (zeroed placeholder) +- Bytes 36-43: Receive mono timestamp (zeroed placeholder) +- Bytes 44-51: Send wall timestamp (zeroed placeholder) +- Bytes 52-59: Send mono timestamp (zeroed placeholder) + +**Notes:** All stats and timestamp fields are zeroed in echo requests. +They serve as placeholders to reach the negotiated packet length. + +--- + +## Vector 4: Echo Reply (no HMAC, default stats+timestamps) + +**Scenario:** Server reply to seq 2, third packet in session. +**Direction:** Server → Client +**Token:** `0x7896b6ab87715213`, Seq: 2, Recv Count: 3, Window: 0x07 + +**Packet (60 bytes):** +``` +14 a7 5b 02 13 52 71 87 ab b6 96 78 02 00 00 00 +03 00 00 00 07 00 00 00 00 00 00 00 b8 1a 33 0c +86 6d aa 18 de 26 35 95 00 00 00 00 80 4d 33 0c +86 6d aa 18 b2 57 35 95 00 00 00 00 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `02` (Reply) +- Bytes 4-11: Connection token +- Bytes 12-15: Sequence number `02 00 00 00` (LE: 2) +- Bytes 16-19: Received count `03 00 00 00` (LE: 3) +- Bytes 20-27: Received window `07 00 00 00 00 00 00 00` (LE: 0x07, bits 0-2 set) +- Bytes 28-35: Receive wall timestamp `b8 1a 33 0c 86 6d aa 18` (LE int64) +- Bytes 36-43: Receive mono timestamp `de 26 35 95 00 00 00 00` (LE int64) +- Bytes 44-51: Send wall timestamp `80 4d 33 0c 86 6d aa 18` (LE int64) +- Bytes 52-59: Send mono timestamp `b2 57 35 95 00 00 00 00` (LE int64) + +**Window interpretation (for seq 2):** +- Bit 0 (0x01): Seq 2 received ✓ +- Bit 1 (0x02): Seq 1 received ✓ +- Bit 2 (0x04): Seq 0 received ✓ + +--- + +## Vector 5: Close Request (no HMAC) + +**Scenario:** Client sends close after test. +**Direction:** Client → Server +**Token:** `0x7896b6ab87715213` + +**Packet (12 bytes):** +``` +14 a7 5b 04 13 52 71 87 ab b6 96 78 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `04` (Close) +- Bytes 4-11: Connection token + +--- + +## Vector 6: HMAC Open Request + +**Scenario:** Client opens session with HMAC key "testkey". +**Direction:** Client → Server +**HMAC key:** ASCII "testkey" (7 bytes) + +**Packet (40 bytes):** +``` +14 a7 5b 09 ff 90 16 a7 aa 53 78 16 9e c3 a2 d5 +54 dc 30 36 01 02 02 80 d0 ac f3 0e 03 80 a8 d6 +b9 07 05 06 06 06 07 06 +``` + +**Field breakdown:** +- Bytes 0-2: Magic `14 a7 5b` +- Byte 3: Flags `09` (Open | HMAC) +- Bytes 4-19: HMAC `ff 90 16 a7 aa 53 78 16 9e c3 a2 d5 54 dc 30 36` +- Bytes 20-39: Parameter payload + +**HMAC verified independently with Python (HMAC-MD5 over entire packet +with HMAC field zeroed).** + +--- + +## Vector 7: HMAC Echo Request + +**Scenario:** Echo request with HMAC, seq 0. +**Direction:** Client → Server +**Token:** `0x4387e9eb5d3fca59`, HMAC key: "testkey" + +**Packet (76 bytes):** +``` +14 a7 5b 08 d9 87 4f 82 b3 13 10 31 59 ad 2c 8f +6b da ef ff 59 ca 3f 5d eb e9 87 43 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 +00 00 00 00 00 00 00 00 00 00 00 00 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `08` (HMAC) +- Bytes 4-19: HMAC +- Bytes 20-27: Connection token `59 ca 3f 5d eb e9 87 43` +- Bytes 28-31: Sequence number `00 00 00 00` +- Bytes 32-75: Stats + timestamp placeholders (zeroed) + +--- + +## Vector 8: HMAC Close Request + +**Scenario:** Close with HMAC. +**Direction:** Client → Server +**Token:** `0x4387e9eb5d3fca59`, HMAC key: "testkey" + +**Packet (28 bytes):** +``` +14 a7 5b 0c f5 cd 0f a9 de 9d 7d 66 8d 91 a5 32 +48 0e 42 e0 59 ca 3f 5d eb e9 87 43 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `0c` (Close | HMAC) +- Bytes 4-19: HMAC +- Bytes 20-27: Connection token + +--- + +## Vector 9: No-Test Mode (Open+Close Request) + +**Scenario:** Client opens and immediately closes. +**Direction:** Client → Server + +**Packet (25 bytes):** +``` +14 a7 5b 05 01 02 02 80 e0 ba 84 bf 03 03 80 a8 +d6 b9 07 05 06 06 06 07 06 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `05` (Open | Close) +- Bytes 4-24: Parameter payload + - Tag 1: ProtocolVersion = 1 + - Tag 2: Duration = 60000000000 ns (60s, default) + - Tag 3: Interval = 1000000000 ns (1s, default) + - Tag 5: ReceivedStats = 3 (Both) + - Tag 6: StampAt = 3 (Both) + - Tag 7: Clock = 3 (Both) + +--- + +## Vector 10: No-Test Mode (Open+Close Reply) + +**Scenario:** Server acknowledges no-test open+close. +**Direction:** Server → Client + +**Packet (33 bytes):** +``` +14 a7 5b 07 00 00 00 00 00 00 00 00 01 02 02 80 +e0 ba 84 bf 03 03 80 a8 d6 b9 07 05 06 06 06 07 +06 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `07` (Open | Reply | Close) +- Bytes 4-11: Connection token `00 00 00 00 00 00 00 00` (ZERO) +- Bytes 12-32: Parameter payload + +**Notes:** In no-test mode, the server returns a zero connection token. +The Close flag in this context means "acknowledged close" not "rejected." + +--- + +## Vector 11: Minimal Echo Packet (no stats, no timestamps) + +**Scenario:** Echo request with stats=none, tstamp=none. +**Direction:** Client → Server +**Token:** `0xa0316fa25c61154e`, Seq: 0 + +**Packet (16 bytes):** +``` +14 a7 5b 00 4e 15 61 5c a2 6f 31 a0 00 00 00 00 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `00` +- Bytes 4-11: Connection token +- Bytes 12-15: Sequence number + +This is the absolute minimum echo packet size. + +--- + +## Vector 12: Midpoint Timestamps (both clocks) + +**Scenario:** Echo reply with stats=both, tstamp=midpoint, clock=both. +**Direction:** Server → Client + +**Packet (44 bytes):** +``` +14 a7 5b 02 91 a4 1e 7a f0 14 f6 62 00 00 00 00 +01 00 00 00 01 00 00 00 00 00 00 00 f7 0e 2c 93 +a2 6d aa 18 5a 5f 14 47 03 00 00 00 +``` + +**Field breakdown:** +- Bytes 0-2: Magic +- Byte 3: Flags `02` (Reply) +- Bytes 4-11: Token +- Bytes 12-15: Seq `00 00 00 00` (0) +- Bytes 16-19: Recv count `01 00 00 00` (1) +- Bytes 20-27: Recv window `01 00 00 00 00 00 00 00` +- Bytes 28-35: Midpoint wall timestamp +- Bytes 36-43: Midpoint mono timestamp + +**Notes:** Only midpoint timestamps present. No receive/send timestamp +fields. Confirms midpoint is mutually exclusive with receive+send.