docs: add clean-room docs/specs and agent guidance

This commit is contained in:
Nils Andreas Svee committed 2026-08-10 18:54:22 +07:00
1 parent 49826a5b3d
commit 186c6eba97
17 files changed
+2752

No files matched your search

+178
View File
@@ -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.
+4
View File
@@ -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.
+17
View File
@@ -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.
+189
View File
@@ -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.
@@ -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 <D> -i <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
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+304
View File
@@ -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.