docs(protocol): add sanitized server compatibility research #67
No files matched your search
@@ -15,13 +15,19 @@ Before editing, also check for a more specific `AGENTS.md` in the target subtree
|
||||
|
||||
Compatibility and interoperability references:
|
||||
|
||||
- `docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md` — verified wire/measurement
|
||||
- `docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md` — verified client-side
|
||||
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/IRTT_SERVER_PROTOCOL_SPEC.md` — verified server-side
|
||||
compatibility baseline.
|
||||
- `docs/protocol/BLACKBOX_VERIFICATION_REPORT.md` — black-box evidence and
|
||||
corrections supporting both the client and server baselines.
|
||||
- `docs/protocol/test-vectors/README.md` — captured client-facing packet
|
||||
vectors.
|
||||
- `docs/protocol/test-vectors/SERVER_BEHAVIORAL_VECTORS.md` — black-box
|
||||
server behavioral vectors, edge cases, and session behavior.
|
||||
- `docs/protocol/captures/` — packet captures referenced by the verification
|
||||
report.
|
||||
report and behavioral vectors. Behavioral vectors and captures are
|
||||
evidence/artifacts, not architecture prescriptions.
|
||||
- `docs/INTEROP_COMPARISON.md` — procedures for black-box comparison against
|
||||
the upstream `irtt` executable.
|
||||
- `docs/clean-room/CLEANROOM_NOTES.md` — provenance only; not a normative
|
||||
@@ -33,9 +39,9 @@ For compatibility questions, the verified spec and black-box observations define
|
||||
|
||||
## Architecture
|
||||
|
||||
Main boundary:
|
||||
Main boundary (`irtt-server` is planned, not yet implemented):
|
||||
|
||||
> `irtt-proto` implements wire semantics. `irtt-client` implements client/session behavior. `irtt-stats` aggregates events. `irtt-cli` presents and orchestrates them.
|
||||
> `irtt-proto` — wire semantics shared by client/server. `irtt-client` — client/session behavior. `irtt-server` — server/session behavior and Tokio runtime/orchestration. `irtt-stats` — statistics over client-facing events/results as currently designed. `irtt-cli` — presentation/orchestration.
|
||||
|
||||
- `irtt-proto`: pure protocol encoding/decoding, flags, layouts, params, varints/zigzag, HMAC-MD5, validation. No sockets, runtimes, lifecycle, stats, or presentation.
|
||||
- `irtt-client`: socket/session lifecycle, negotiation, probe send/receive, timing metadata, classification, bounded state, and events.
|
||||
@@ -43,12 +49,25 @@ Main boundary:
|
||||
- `AsyncClient`: optional Tokio low-level adapter.
|
||||
- `ManagedClientTask` / `ManagedClientHandle`: unified Tokio managed driver/control surface.
|
||||
- `BlockingManagedClient`: synchronous owner using a dedicated current-thread Tokio runtime.
|
||||
- `irtt-server`: planned; see "Server architecture (`irtt-server`)" below.
|
||||
- `irtt-stats`: cumulative/rolling/finite statistics over client events. No wire/session state machine logic.
|
||||
- `irtt-cli`: argument parsing, managed orchestration, output, summaries, TUI, diagnostics, and exit behavior. Do not duplicate protocol/session state machines here.
|
||||
|
||||
Tokio must remain optional for `irtt-client`; default builds must remain runtime-free and Tokio-free unless an explicit architectural change says otherwise.
|
||||
Tokio must remain optional for `irtt-client`; default builds must remain runtime-free and Tokio-free unless an explicit architectural change says otherwise. This rule is specific to `irtt-client` — do not generalize it to `irtt-server`, which has the opposite policy below.
|
||||
|
||||
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.
|
||||
### Server architecture (`irtt-server`)
|
||||
|
||||
`irtt-server` is a planned first-class reusable server crate. It does not exist yet; this section describes the intended boundary for it, not an implemented API.
|
||||
|
||||
- It shares wire semantics with the client through `irtt-proto` and follows the same clean-room boundary.
|
||||
- It must **not** depend on `irtt-client` merely to reuse protocol behavior.
|
||||
- Unlike `irtt-client`, the server has no product requirement to remain runtime-free: `irtt-server` is intentionally **Tokio-native**, and Tokio does not need to be optional for it.
|
||||
- There is currently no requirement for a blocking server API, alternate async runtimes, a runtime abstraction layer, or a runtime-free server feature. Do not build blocking/runtime variants merely for symmetry with the client, and do not invent a mirrored client-style API family (`BlockingServer`, `AsyncServer`, `ManagedServer`, `BlockingManagedServer`, or similar) ahead of agreement.
|
||||
- Protocol/session logic should be separated from socket/runtime orchestration where that materially improves deterministic testing, ownership clarity, state-machine reasoning, or maintainability. A `ServerCore`-like internal boundary is therefore desirable, but its exact name/API is not prescribed, it need not be public, it is not a promise of runtime independence, and simple code should not be tortured merely to make the core artificially pure.
|
||||
- Runtime/server orchestration belongs around that core and may freely use Tokio for UDP sockets, `recv_from`/`send_to`, IPv4/IPv6 listeners, timers, session expiry, max-duration deadlines, shutdown, bounded control/config channels where useful, and `select!`-style orchestration.
|
||||
- The CLI server applet, when added, should remain thin orchestration/configuration over reusable `irtt-server`, not contain a second server state machine.
|
||||
- Resource policy: the server should use bounded session/resource policy. Upstream's observed lack of useful default session/per-peer bounds (see `IRTT_SERVER_PROTOCOL_SPEC.md`) is not an interoperability requirement and must not be reproduced merely for compatibility.
|
||||
- Testing: once a real `irtt-server` exists, prefer it for normal compliant fake-peer behavior where practical. Keep narrow adversarial/raw peers for intentionally malformed, ambiguous, or non-compliant wire behavior.
|
||||
|
||||
## Engineering rules
|
||||
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
|
||||
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.
|
||||
`irtt-proto` is the pure protocol boundary. Keep it free of sockets, runtimes, client/server lifecycle, session tables, statistics, and presentation concerns.
|
||||
|
||||
As `irtt-server` is built, it may need server-facing wire-inspection
|
||||
primitives here, such as request decoding, reply encoding, packet/header
|
||||
validation, HMAC-presence inspection, and token/header inspection before
|
||||
Params-dependent decoding.
|
||||
|
||||
For protocol behavior, consult before changing semantics:
|
||||
|
||||
- `docs/protocol/IRTT_CLIENT_PROTOCOL_SPEC.md`
|
||||
- `docs/protocol/IRTT_SERVER_PROTOCOL_SPEC.md`
|
||||
- `docs/protocol/BLACKBOX_VERIFICATION_REPORT.md`
|
||||
- `docs/protocol/test-vectors/README.md`
|
||||
- `docs/protocol/test-vectors/SERVER_BEHAVIORAL_VECTORS.md`
|
||||
|
||||
Protocol changes that affect server-side parsing, encoding, or session-facing wire semantics must consult the server spec and behavioral vectors as well as the client-side material above.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -2,10 +2,41 @@
|
||||
|
||||
## 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.
|
||||
This document records the clean-room boundary for the IRTT 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.
|
||||
|
||||
It covers three passes and a final sanitization:
|
||||
|
||||
- the **client** pass (2026-04-28), producing
|
||||
`../protocol/IRTT_CLIENT_PROTOCOL_SPEC.md`;
|
||||
- the **server** pass (2026-08-11), producing
|
||||
`../protocol/IRTT_SERVER_PROTOCOL_SPEC.md` and its behavioral vectors, and
|
||||
applying corrections back to the client specification;
|
||||
- the **midpoint timestamp follow-up** (2026-08-11);
|
||||
- the **final sanitization pass** (2026-08-11), recorded at the end of this file.
|
||||
|
||||
## How to Read This File
|
||||
|
||||
This is a provenance record. It states *what class of material was excluded* and
|
||||
*what discipline was applied*; it deliberately does **not** restate the excluded
|
||||
material. A note that says "an upstream identifier was removed" and then quotes
|
||||
the identifier has removed nothing. Earlier revisions of this file made exactly
|
||||
that mistake, and the audit sections below have been rewritten to describe
|
||||
categories rather than instances.
|
||||
|
||||
Two things that are *not* contamination, recorded here so they are not
|
||||
re-flagged:
|
||||
|
||||
- **Parameter names.** ProtocolVersion, Duration, Interval, Length,
|
||||
ReceivedStats, StampAt, Clock, DSCP and ServerFill appear throughout the
|
||||
specifications. They come from upstream's **published** documentation of its
|
||||
client's machine-readable output, not from source inspection.
|
||||
- **Quoted diagnostic text.** Where a document quotes a message printed by the
|
||||
upstream client or server, that is transcribed program output observed during
|
||||
testing — the same thing any black-box tester sees — and is cited as evidence
|
||||
of observed behavior.
|
||||
|
||||
## Clean-Room Boundary
|
||||
|
||||
@@ -13,23 +44,21 @@ to the GPL-licensed upstream source code.
|
||||
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
|
||||
- **Clean side:** The implementation agent will receive only the documents
|
||||
under `docs/protocol/` and this notes file. 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:
|
||||
The following materials from the upstream IRTT project (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
|
||||
1. **Published end-user documentation:** the project overview and FAQ, the
|
||||
general and per-command manual pages, and the changelog. This material is
|
||||
public and may be relied on directly; it is cited in the specifications where
|
||||
used.
|
||||
|
||||
2. **Source files (inspected for protocol behavior only):**
|
||||
2. **Source (inspected for protocol behavior only):**
|
||||
- Protocol version and constants
|
||||
- Packet format, field layout, magic bytes, flags
|
||||
- Parameter serialization format (tag-value pairs, varint encoding)
|
||||
@@ -64,66 +93,60 @@ copied.
|
||||
|
||||
## Post-Drafting Audit
|
||||
|
||||
After the initial specification was drafted, a clean-room audit was
|
||||
performed. The following issues were identified and corrected:
|
||||
After the initial specification was drafted, a clean-room audit was performed.
|
||||
The categories of problem found and corrected were:
|
||||
|
||||
1. **Upstream private type names leaked:** "ctoken" and "seqno" appeared
|
||||
as parenthetical abbreviations in the terminology table. Removed.
|
||||
1. **Upstream private abbreviations in the terminology table.** Removed and
|
||||
replaced with descriptive names.
|
||||
|
||||
2. **Upstream implementation preference disclosed:** A note about IPv4
|
||||
preference in dual-stack resolution revealed an upstream implementation
|
||||
choice. Removed.
|
||||
2. **An upstream implementation preference disclosed** in the address-resolution
|
||||
discussion. Removed; the spec now leaves the choice to the implementer.
|
||||
|
||||
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).
|
||||
3. **Implementation-language standard-library references** in the parameter
|
||||
encoding section. 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).
|
||||
4. **An internal scheduling algorithm described** in the send-scheduling section,
|
||||
mirroring upstream control flow. Replaced with observable behavior: packets
|
||||
are sent at approximately their ideal times and drift compensation is
|
||||
implementation-defined.
|
||||
|
||||
5. **Upstream timer algorithm named:** "The upstream uses exponential
|
||||
averaging of timer errors" revealed an internal implementation choice.
|
||||
Removed.
|
||||
5. **An upstream timer-smoothing strategy named.** 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.
|
||||
6. **An RTT edge-case guard asserted** that upstream does not in fact 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.
|
||||
7. **Received-window validity asserted normatively** when it was
|
||||
verification-required. Softened with a reference to Open Questions.
|
||||
|
||||
8. **Go concurrency term leaked:** "goroutine" appeared in the result model
|
||||
section. Replaced with "operation."
|
||||
8. **An implementation-language concurrency term** in the result-model section.
|
||||
Replaced with a neutral word.
|
||||
|
||||
9. **Upstream test design referenced:** Section 18.2 described what "the
|
||||
upstream test" was testing internally. Rewritten to describe the test
|
||||
vector neutrally.
|
||||
9. **Upstream test internals referenced** in the test-vector section. Rewritten
|
||||
to describe the 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.
|
||||
10. **Internal constants asserted as protocol requirements** — a minimum
|
||||
restricted interval and a maximum parameter buffer size. Moved to Open
|
||||
Questions with verification suggestions. Both were subsequently settled by
|
||||
measurement (Sections 19.9 and 19.10 of the client specification).
|
||||
|
||||
11. **Timestamp capture order stated as MUST:** Implementation-level timing
|
||||
advice was stated as a protocol requirement. Changed to SHOULD with
|
||||
measurement-accuracy rationale.
|
||||
11. **Timestamp capture order stated as MUST** when it was implementation-level
|
||||
timing advice. Downgraded, and later rewritten again in the final
|
||||
sanitization pass to describe only the externally relevant relationship.
|
||||
|
||||
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.
|
||||
12. **An internal recording structure described** in the send-timing discussion.
|
||||
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.
|
||||
13. **A self-contradictory inline correction note** that 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.
|
||||
14. **Field ordering stated without a verification caveat** despite being
|
||||
source-derived at the time. Verification note added, and the ordering was
|
||||
subsequently confirmed by capture across six field combinations
|
||||
(Section 19.11).
|
||||
|
||||
No instance of the removed material is reproduced here. Restating a private
|
||||
identifier in order to record that it was removed would defeat the removal.
|
||||
|
||||
## Clean-Room Compliance Checklist
|
||||
|
||||
@@ -145,45 +168,445 @@ performed. The following issues were identified and corrected:
|
||||
|
||||
## 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:
|
||||
A second review was performed on 2026-04-28 by an agent that had not seen the
|
||||
upstream source. The categories of problem found and corrected were:
|
||||
|
||||
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.
|
||||
1. **An implementation-architecture directive in the scope section**, which
|
||||
prescribed internal structure. Removed; the specification now constrains only
|
||||
observable behavior.
|
||||
|
||||
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
|
||||
2. **A concurrency architecture prescribed** in the active-test section,
|
||||
implying an internal threading or task model. Rewritten to state the
|
||||
observable requirement — sending and receiving proceed 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).
|
||||
3. **Source-derived design rationale** offered for the zeroed placeholder fields
|
||||
in an echo request. Simplified to 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.
|
||||
4. **A parenthetical revealing source-exhaustive knowledge** of the complete
|
||||
parameter set. Removed.
|
||||
|
||||
5. **Upstream CLI syntax leaked:** Section 10.10 used `NxD`/`NrD`/`D`
|
||||
notation for wait variants, which mirrors upstream CLI option syntax.
|
||||
5. **Upstream CLI option syntax mirrored** in the wait-variant notation.
|
||||
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."
|
||||
6. **A named statistics algorithm recommended.** Replaced with a statement that
|
||||
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."
|
||||
7. **An implementation-specific error category** that implied a particular
|
||||
allocation strategy. Generalized.
|
||||
|
||||
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.
|
||||
8. **Implementation-language references in this notes file itself.** Replaced
|
||||
with language-neutral phrasing.
|
||||
|
||||
As above, the removed instances are described by category and not reproduced.
|
||||
|
||||
---
|
||||
|
||||
## Server Pass (2026-08-11)
|
||||
|
||||
### Scope
|
||||
|
||||
A second contaminated-side pass characterised the upstream **server** and
|
||||
produced `../protocol/IRTT_SERVER_PROTOCOL_SPEC.md`, together with
|
||||
`../protocol/test-vectors/SERVER_BEHAVIORAL_VECTORS.md` and four new captures.
|
||||
Corrections and clarifications arising from it were applied to the client
|
||||
specification.
|
||||
|
||||
### Upstream Material Inspected
|
||||
|
||||
Three upstream builds are involved in this work, and the sanitized output names
|
||||
whichever one it means everywhere it matters:
|
||||
|
||||
| Build | Role |
|
||||
|-------|------|
|
||||
| **0.9.1 release** | the behavioral baseline; every unqualified statement refers to it |
|
||||
| **0.9.0 release** | comparison only, for the version difference in the midpoint follow-up |
|
||||
| **development tree, six upstream commits past the 0.9.1 tag** | comparison only; post-release work, **not** part of 0.9.1, and never allowed to redefine the baseline |
|
||||
|
||||
*Provenance correction (final sanitization pass):* an earlier revision described
|
||||
that development tree as "eight commits past the 0.9.1 tag". The count was wrong
|
||||
— it included two commits belonging to this clean-room project itself, which were
|
||||
present in the same working tree but are not upstream work. The upstream count is
|
||||
**six**, and the outgoing documents now say so consistently.
|
||||
|
||||
Inspection covered, at a high level:
|
||||
|
||||
1. **Published documentation:** the server manual page, the changelog, and the
|
||||
project overview's security, FAQ and roadmap sections.
|
||||
2. **Source (for behavior only):** request admission and validation, session
|
||||
creation and lookup, parameter restriction, echo handling, reception
|
||||
statistics, timestamp selection, payload filling, close handling, session
|
||||
expiry, and server configuration defaults.
|
||||
3. **Release history:** tags and commits between 0.9.1 and the checked-out
|
||||
development head, to classify version differences.
|
||||
|
||||
Source inspection was used only to decide **what to test**. Every normative
|
||||
statement in the sanitized output rests on an observed experiment, a capture, or
|
||||
published end-user documentation — not on the source.
|
||||
|
||||
### Method, and What Each Part of It Establishes
|
||||
|
||||
The distinction below is load-bearing and is preserved in the outgoing documents.
|
||||
|
||||
- **Black-box observation — a raw UDP harness.** It drove a real upstream server
|
||||
directly, so that requests no conforming client would emit could be tested
|
||||
(out-of-order sequence numbers, truncated packets, invalid flag combinations,
|
||||
foreign-endpoint packets, corrupted authentication, and so on). It shares no
|
||||
code with any implementation. Its measurements are stated as observed wire
|
||||
behavior.
|
||||
- **Black-box observation — the real upstream client.** Driving the unmodified
|
||||
upstream client and observing what it recorded, reported and exited with is an
|
||||
observation of a genuinely independent second implementation.
|
||||
- **Measurement, not constant-reading.** Timing boundaries (idle-expiry grace,
|
||||
maximum-duration grace, interval caps) were established by measuring the
|
||||
server's responses, not by reading values out of the source.
|
||||
- **Contaminated-side consistency validation — a server written on this side.**
|
||||
A server was built from the behavioral model and driven by the real upstream
|
||||
client across eleven configurations. Because it was written on the contaminated
|
||||
side it is **not** an independent implementation, and nothing in the outgoing
|
||||
documents rests on it. What it establishes is that the specification as written
|
||||
is sufficient to build something the upstream client accepts; the client's
|
||||
acceptance is the black-box part. Earlier revisions called this an "independent
|
||||
server"; that description was inaccurate and has been corrected throughout.
|
||||
- All research scripts, logs, probe sources and notes were kept outside `docs/`
|
||||
and are not part of the outgoing material.
|
||||
|
||||
### Statement of Non-Copying (Server Pass)
|
||||
|
||||
No upstream source code, comments, pseudocode, tests, or file/module names were
|
||||
copied into any document under `docs/`. The sanitized output describes
|
||||
externally observable behavior only.
|
||||
|
||||
Where upstream behavior differs from what a naive reading of the wire format
|
||||
would suggest — the received-window reset on reordering, the unenforced protocol
|
||||
version, the midpoint dual-field emission — that difference is documented as an
|
||||
**observation**, with the input that produces it and the output it yields. No
|
||||
explanation of *why* the implementation behaves that way appears in the
|
||||
sanitized output, because any such explanation would necessarily be
|
||||
source-derived.
|
||||
|
||||
*Amended 2026-08-11:* this remains true of the received-window and
|
||||
protocol-version items. For the dual-field midpoint emission, one narrowly scoped
|
||||
**source-assisted historical conclusion** was admitted and labelled — a judgement
|
||||
that the measured 0.9.0-to-0.9.1 difference was unintended. See "Midpoint
|
||||
Timestamp Follow-Up" below.
|
||||
|
||||
Two upstream robustness defects are recorded in the server specification
|
||||
(`../protocol/IRTT_SERVER_PROTOCOL_SPEC.md` Section 22) because both are
|
||||
reachable from ordinary client traffic and a clean implementation has to decide
|
||||
what to do instead. They are stated as observed outcomes plus **robustness
|
||||
recommendations for the clean implementation** — explicitly not as
|
||||
interoperability requirements, since no conforming client can observe which
|
||||
choice a server made — with no description of the internal cause.
|
||||
|
||||
### Server-Pass Sanitization Audit
|
||||
|
||||
The following were checked for and are absent from all files under `docs/`:
|
||||
|
||||
1. Upstream private function, type, field and variable names.
|
||||
2. Upstream source file names, module names and package names.
|
||||
3. Implementation-language identifiers, standard-library references and
|
||||
concurrency vocabulary.
|
||||
4. Copied or paraphrased upstream comments.
|
||||
5. Source-shaped pseudocode and control-flow transcription.
|
||||
6. Internal constants that are not externally observable. Timing values that
|
||||
*are* observable (the idle-expiry grace, the maximum-duration grace, the
|
||||
interval cap ratio) are stated with the measurement that establishes them and
|
||||
are labelled as server policy, not protocol law.
|
||||
7. Internal data-structure descriptions. Session lookup is described purely in
|
||||
terms of which packets are accepted and which are dropped.
|
||||
8. Token generation strategy. The specification states only the uniqueness
|
||||
requirement and explicitly leaves generation to the implementer.
|
||||
|
||||
The following items were considered and deliberately **excluded** from the
|
||||
sanitized output as implementation-internal:
|
||||
|
||||
- how session state is stored, indexed, or reclaimed;
|
||||
- how packet buffers are managed or reused;
|
||||
- the mechanism by which expired sessions are noticed;
|
||||
- any description of why the received-window behavior takes the form it does.
|
||||
|
||||
### Clean-Room Compliance Checklist (Server Pass)
|
||||
|
||||
- [x] No upstream source code included.
|
||||
- [x] No upstream comments quoted.
|
||||
- [x] No upstream private function or type names included.
|
||||
- [x] No upstream file/module layout described.
|
||||
- [x] No implementation-language references remain.
|
||||
- [x] No pseudocode derived from upstream implementation.
|
||||
- [x] No internal data structures or algorithms described.
|
||||
- [x] Observable behavior separated from server policy throughout.
|
||||
- [x] Every normative statement backed by an experiment, capture, or public
|
||||
documentation.
|
||||
- [x] Version differences classified explicitly, with each build named (server
|
||||
specification Section 21).
|
||||
- [x] Unresolved questions recorded rather than inferred (server
|
||||
specification Section 23).
|
||||
- [x] Contaminated-side consistency validation performed against the real
|
||||
upstream client, and labelled as such rather than as independent
|
||||
verification.
|
||||
- [x] Contaminated research notes, scripts and probe sources kept outside
|
||||
`docs/`.
|
||||
|
||||
---
|
||||
|
||||
## Midpoint Timestamp Follow-Up (2026-08-11)
|
||||
|
||||
### Scope
|
||||
|
||||
A focused contaminated-side investigation of one behavior: the timestamp layout
|
||||
upstream 0.9.1 emits for StampAt = Midpoint. It produced changes to the server
|
||||
specification (Sections 9.2, 11.3.1, 21.1, 21.2, 23.4, 24), the client
|
||||
specification (Sections 8.5, 19.14), the verification report (Part III,
|
||||
Finding S-18, plus a caveat on Section 19.2) and the behavioral vectors
|
||||
(Section 5.2 and its new subsections).
|
||||
|
||||
### A Different Provenance Mix From the Earlier Passes
|
||||
|
||||
The earlier passes used source inspection **only to decide what to test**. This
|
||||
pass went further: contaminated-side analysis was used to form expectations,
|
||||
which were then tested. The sanitized output therefore contains one class of
|
||||
statement the earlier passes did not, and it is labelled wherever it appears.
|
||||
What crosses the boundary falls into four kinds:
|
||||
|
||||
- **Black-box observed** — the wire order (wall field then monotonic field), the
|
||||
affected StampAt/Clock combinations, the dependence of the reply length on the
|
||||
negotiated length, the value an ordinary positional decoder reads in the
|
||||
monotonic-only case, and the 0.9.0 / 0.9.1 / tested-post-0.9.1 comparison. All
|
||||
measured with a raw UDP harness against three separate server builds, and
|
||||
cross-checked at the application layer with the real upstream client.
|
||||
- **Source-assisted historical conclusion** — one judgement only: that the
|
||||
measured difference between 0.9.0 and 0.9.1 is an unintended regression rather
|
||||
than a designed feature. It is labelled where it appears, it is not normative,
|
||||
and the measured version difference is stated separately and stands on its own.
|
||||
- **Interoperability requirement** — narrowly, what a clean client must tolerate
|
||||
for this one verified case.
|
||||
- **Robustness / implementation recommendation** — what a clean server should
|
||||
emit.
|
||||
|
||||
The specification wording keeps these four apart explicitly, so a clean-side
|
||||
reader can tell which statements they may rely on as observed fact.
|
||||
|
||||
Note in particular what is **not** in the list. An earlier revision also carried
|
||||
a characterisation of how upstream conceptually represents a midpoint value.
|
||||
That was an internal model, it was source-derived, and the useful wire behavior
|
||||
is fully established without it, so it was removed from all implementation-facing
|
||||
documentation in the final sanitization pass.
|
||||
|
||||
### What Was Deliberately Not Transferred
|
||||
|
||||
Detailed upstream implementation structure, internal representation, control
|
||||
flow, change-level source evidence, and the source-derived reasoning supporting
|
||||
the regression diagnosis were retained only on the contaminated side.
|
||||
|
||||
That sentence is the whole of the disclosure, and deliberately so. An earlier
|
||||
revision of this file enumerated the withheld findings in enough detail that
|
||||
reading the list conveyed much of what the list claimed to be withholding. Naming
|
||||
the categories is what a provenance record needs; restating the contents defeats
|
||||
the exclusion it is recording.
|
||||
|
||||
What crossed the boundary from that body of work is one sentence of conclusion —
|
||||
labelled source-assisted, non-normative, and separable from every requirement in
|
||||
the outgoing documents.
|
||||
|
||||
### Method
|
||||
|
||||
- A raw UDP harness performed the open negotiation, sent a single echo request,
|
||||
and reported the exact reply datagram length and the bytes at each timestamp
|
||||
offset. It shares no code with any implementation.
|
||||
- Three server builds were exercised, each named wherever its results are used:
|
||||
the **0.9.1 release**, the **0.9.0 release**, and a build of the upstream
|
||||
development tree **six upstream commits past the 0.9.1 tag**. Builds and
|
||||
harness live outside this tree.
|
||||
- The real upstream client was used as a second, genuinely independent observer
|
||||
for the application-layer effects.
|
||||
|
||||
### Clean-Room Compliance Checklist (Midpoint Follow-Up)
|
||||
|
||||
- [x] No upstream source code, comments or pseudocode included.
|
||||
- [x] No upstream private function, type or field names included.
|
||||
- [x] No upstream control flow described.
|
||||
- [x] No upstream file or module layout described.
|
||||
- [x] No internal conceptual model of upstream's timestamp representation
|
||||
included.
|
||||
- [x] The single source-assisted statement labelled as such, confined to a
|
||||
historical judgement, and never presented as black-box verified.
|
||||
- [x] Black-box observation, source-assisted conclusion, interoperability
|
||||
requirement and robustness recommendation kept distinct.
|
||||
- [x] Excluded contaminated-side material described by category only, without
|
||||
restating its content.
|
||||
- [x] Earlier over-general claim ("always 8 bytes longer") corrected rather than
|
||||
left standing.
|
||||
|
||||
---
|
||||
|
||||
## Final Sanitization Pass (2026-08-11)
|
||||
|
||||
A last audit of the entire outgoing `docs/` tree before it crosses the airlock.
|
||||
No new protocol research was performed; this pass only corrected, reclassified
|
||||
and removed.
|
||||
|
||||
### Provenance
|
||||
|
||||
- Every statement now names the build it came from. The **0.9.1 release** is the
|
||||
baseline; the **0.9.0 release** and the **development tree six upstream commits
|
||||
past the 0.9.1 tag** appear only where explicitly labelled, and neither is
|
||||
allowed to stand in for 0.9.1.
|
||||
- The "eight commits past the 0.9.1 tag" figure was corrected to **six**. The
|
||||
earlier count wrongly included two commits belonging to this clean-room project
|
||||
itself.
|
||||
- The server written on this side is now described everywhere as **contaminated-
|
||||
side consistency validation**, never as an independent implementation. Only the
|
||||
raw UDP harness and the real upstream client are treated as black-box
|
||||
observers.
|
||||
|
||||
### Statement classification
|
||||
|
||||
The outgoing documents now separate five kinds of statement and never conflate
|
||||
them: interoperability requirement; black-box observed upstream behavior;
|
||||
upstream policy or default; robustness recommendation for the clean
|
||||
implementation; and source-assisted historical conclusion.
|
||||
|
||||
Specific reclassifications made in this pass:
|
||||
|
||||
- Surviving a failed reply send is a **robustness recommendation**, not a
|
||||
protocol MUST. No conforming client can observe which choice a server made.
|
||||
- Upstream's absence of session and per-peer bounds is **observed policy**, and
|
||||
the documents now say explicitly that it is not something a compatible server
|
||||
should reproduce. The clean server is expected to be bounded; the shape of that
|
||||
policy is left to the clean project.
|
||||
- The fill-validation, DSCP-clamping and length-clamping items in the hazards
|
||||
section were likewise moved from "requirement" to robustness recommendation.
|
||||
- The server specification's conformance summary is split: interoperability
|
||||
MUST / MUST NOT lists, and a separate list of robustness recommendations.
|
||||
|
||||
### Architecture neutrality
|
||||
|
||||
The outgoing documents prescribe no runtime or architecture for the clean server
|
||||
— no blocking-versus-async choice, no runtime-free API, no core-type shape, no
|
||||
socket ownership arrangement. Those are clean-project decisions and belong in the
|
||||
clean repository after the airlock, not in protocol documentation. The
|
||||
architecture-neutrality statement in the server specification's scope section was
|
||||
extended to say so directly.
|
||||
|
||||
### Wording removed or rewritten
|
||||
|
||||
- **Source-derived timestamp placement.** Statements locating a timestamp
|
||||
immediately before or after a particular system call were replaced with the
|
||||
externally relevant property: the pair must bracket the server's handling of
|
||||
the request, receive never later than send. Where an implementation takes its
|
||||
readings is not observable and is no longer described.
|
||||
- **Validation-order narrative.** The packet-admission section was rewritten from
|
||||
a numbered processing sequence into a table of discard conditions plus the two
|
||||
ordering relationships that were actually verified externally (Open-versus-Close
|
||||
and Open-versus-echo-shaped-body interpretation precedence for an otherwise
|
||||
admissible datagram, which overrides no independent rejection condition; and the
|
||||
indistinguishability of an authentication failure from an unknown token).
|
||||
Behavioral wording replaced control-flow wording throughout.
|
||||
- **Internal midpoint model.** The characterisation of upstream's internal
|
||||
representation of a midpoint timestamp was removed from all
|
||||
implementation-facing documentation. What remains is the wire fact: a
|
||||
dual-field midpoint representation, wall field followed by monotonic field.
|
||||
- **DSCP.** The wire parameter is now stated plainly as the raw IP TOS /
|
||||
Traffic-Class byte, range 0–255, with Expedited Forwarding at 0xb8 / 184. The
|
||||
future user-facing notation for the clean project is marked out of scope rather
|
||||
than baked into the protocol documents.
|
||||
|
||||
### Corrected claims
|
||||
|
||||
- **Midpoint reply length.** Every "negotiated length + 8" and "normal packet
|
||||
length + 8" claim was removed. The documents now state the model uniformly:
|
||||
`upstream_header` is `normal_header` plus one 8-byte timestamp field;
|
||||
`compatible_reply_len = max(negotiated_length, normal_header)`;
|
||||
`upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)`; and the
|
||||
observable excess is +8, then +7…+1, then 0 as the negotiated length rises. The
|
||||
measured sweeps demonstrating this were preserved.
|
||||
- **Overlong replies.** The blanket requirement that a client tolerate any reply
|
||||
longer than the negotiated length was narrowed to the one verified case:
|
||||
single-clock midpoint at **exactly** `upstream_0_9_1_reply_len`. That is one
|
||||
additional accepted length per negotiation, not a `+1..+8` window — where
|
||||
`upstream_0_9_1_reply_len > compatible_reply_len` the two values differ by
|
||||
between 1 and 8 bytes, but a length strictly between them is rejected like any
|
||||
other unexpected length. The documents say explicitly that this does not
|
||||
generalise to arbitrary overlong echo replies and that strict validation of
|
||||
other malformed or oversized packets is retained.
|
||||
- **Normal minimum reply size.** Reply-length validation is stated against
|
||||
`compatible_reply_len`, the normal compatible reply length, rather than against
|
||||
the negotiated `Length` value alone; the two differ whenever the negotiated
|
||||
Length is below the mandatory field block.
|
||||
- **Monotonic-only midpoint.** The suggestion that a client may simply read the
|
||||
second 8-byte region as the monotonic value was replaced with a conservative
|
||||
rule. In the equal-length regime the conforming and upstream forms are
|
||||
indistinguishable, so the second region may be ordinary payload; a correction
|
||||
is permitted only where the dual-field form is otherwise identifiable, the
|
||||
ambiguity is recorded rather than resolved by heuristic, and the packet must be
|
||||
accepted either way.
|
||||
- **0.9.0 → 0.9.1.** Any claim of no protocol-visible change between the two was
|
||||
corrected. Published release-note content and measured wire behavior are now
|
||||
stated separately, and they disagree on this point.
|
||||
- **Protocol version.** Stale text using "protocol version mismatch" as an
|
||||
example of a normal server rejection was removed. Upstream 0.9.1 accepts
|
||||
version 0, 2, negative and absent, and returns 1; enforcement is client-side.
|
||||
- **Received window.** The documents now state that `0x1` means no useful prior
|
||||
received-history is represented, **not** that the previous 63 packets were
|
||||
lost. Downstream/upstream loss classification is flagged as a separate question
|
||||
scheduled for its own clean-side audit.
|
||||
- **Close semantics.** No production standalone close-reply codec is specified.
|
||||
Such a datagram was observed to be tolerated by upstream clients but has no
|
||||
demonstrated purpose in protocol version 1, and the documents say so.
|
||||
- **Parameter payload size.** The "believed to be 128 bytes" limit was replaced
|
||||
with the measured result: no protocol limit on a received payload; 128 bytes is
|
||||
a local allocation choice.
|
||||
|
||||
### Contamination scan
|
||||
|
||||
The whole outgoing `docs/` tree was scanned against a private-token list derived
|
||||
on the contaminated side from the upstream tree. That list is **not** reproduced
|
||||
here, and neither is any token from it.
|
||||
|
||||
Scanned for and now absent: upstream private function, type, field and variable
|
||||
identifiers; implementation-language identifiers and standard-library references;
|
||||
upstream source filenames, module and package names; source-derived commit
|
||||
identifiers; local filesystem paths; scratchpad paths; and temporary build paths.
|
||||
|
||||
The residual hits found and fixed in this pass were all in the audit prose of
|
||||
this file, which had been quoting the identifiers it claimed to have removed, and
|
||||
one tooling-environment block in the verification report that carried a local
|
||||
installation path and an implementation-language version string.
|
||||
|
||||
Retained deliberately, and not contamination: the public upstream project
|
||||
identity (`heistp/irtt`) where provenance requires it; the parameter names, which
|
||||
come from upstream's published documentation; and quoted upstream program output,
|
||||
which is observed black-box evidence.
|
||||
|
||||
### Captures
|
||||
|
||||
Every `.pcapng` intended to cross was metadata-reviewed. Four had already been
|
||||
container-normalized during the server pass; the remaining ten still carried
|
||||
capture-machine hardware and operating-system identification, the local capture
|
||||
interface name and description, and the capture filter string. All ten were
|
||||
regenerated to strip that metadata.
|
||||
|
||||
No capture contained a hostname, a username, a filesystem path, a command string
|
||||
or any reference to an upstream checkout.
|
||||
|
||||
**Packet bytes were not altered.** For each regenerated file the full frame
|
||||
hexdump and the per-frame epoch timestamps were compared before and after and
|
||||
were identical. No capture had to be excluded from the outgoing set.
|
||||
|
||||
### Outgoing tree hygiene
|
||||
|
||||
The outgoing `docs/` tree contains only specifications, the verification report,
|
||||
behavioral and packet vectors, and captures. No research scripts, probe sources,
|
||||
upstream source archives, build outputs, temporary logs, dirty source notes or
|
||||
private-identifier lists are present, and the contaminated-side material for the
|
||||
midpoint work was left where it already lives, outside `docs/`.
|
||||
|
||||
One incidental local file sits inside `docs/` and is **not** outgoing material: a
|
||||
local tool-permissions file under a dotted directory. It is ignored by version
|
||||
control, it is not tracked, and it may not be copied across the airlock. The
|
||||
macOS Finder metadata file previously listed here is no longer present; the
|
||||
exclusion still stands, since Finder can recreate one at any time. Re-verified
|
||||
2026-08-11: the dotted directory is the only ignored entry under `docs/`, and no
|
||||
Finder metadata file exists anywhere in the outgoing tree.
|
||||
@@ -10,23 +10,22 @@
|
||||
|
||||
## Environment Check
|
||||
|
||||
```
|
||||
$ command -v irtt
|
||||
/Users/lochnair/.local/bin/irtt
|
||||
Tooling used for Part I. Installation paths are omitted deliberately; only the
|
||||
versions matter for reproducing these results.
|
||||
|
||||
```
|
||||
$ 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).
|
||||
TShark (Wireshark) 4.6.4
|
||||
```
|
||||
|
||||
Reference server and client: the upstream **0.9.1 release**, unmodified.
|
||||
Capture platform: macOS Darwin 25.3.0 arm64, loopback interface.
|
||||
|
||||
---
|
||||
|
||||
## Open Question Resolution
|
||||
@@ -126,6 +125,11 @@ Timestamp field count depends on StampAt and Clock:
|
||||
This is a **normative requirement** — implementations must compute the
|
||||
correct minimum to avoid malformed packets.
|
||||
|
||||
**Caveat added 2026-08-11:** the two single-clock midpoint rows describe the
|
||||
*negotiated* layout, which is what a compatible implementation emits. Upstream
|
||||
0.9.1 emits 2 fields (16 bytes) for midpoint with any clock; see Findings S-7 and
|
||||
S-18.
|
||||
|
||||
---
|
||||
|
||||
### 19.4 Varint Encoding — RESOLVED
|
||||
@@ -154,7 +158,8 @@ verified against known values.
|
||||
|
||||
All decoded correctly using standard protobuf-style zigzag + LEB128.
|
||||
|
||||
**Verified independently with Python decoder.**
|
||||
**Cross-checked with a separately written Python decoder**, implemented from the
|
||||
wire-format description alone.
|
||||
|
||||
**Conclusion:** The varint encoding is standard protobuf-compatible. This
|
||||
is a **normative requirement** for interoperability.
|
||||
@@ -166,8 +171,8 @@ is a **normative requirement** for interoperability.
|
||||
**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.
|
||||
**Test setup:** Captured HMAC session, extracted packet bytes, and recomputed
|
||||
HMAC-MD5 with a separately written Python script.
|
||||
|
||||
**Commands:**
|
||||
```
|
||||
@@ -228,6 +233,11 @@ or a bug.
|
||||
can be used as an invalidity sentinel. This is a **compatibility
|
||||
recommendation** — implementations SHOULD treat window=0 as invalid.
|
||||
|
||||
**Superseded in part by Finding S-2 (Part II).** Upstream 0.9.1 was later
|
||||
confirmed never to emit 0 at all when the window is negotiated. The value that
|
||||
actually needs care is `0x1`, which is a *valid* window carrying no history and
|
||||
must not be read as "nothing earlier was received".
|
||||
|
||||
---
|
||||
|
||||
### 19.8 HMAC Mismatch Behavior — RESOLVED
|
||||
@@ -308,6 +318,11 @@ ServerFill string (32 bytes max), the parameter buffer stays well under
|
||||
Reclassified as **compatibility recommendation** — implementations should
|
||||
allocate at least 128 bytes for the parameter buffer.
|
||||
|
||||
**Superseded by Part II.** Upstream servers were later shown to accept an open
|
||||
request carrying over 200 bytes of parameters. There is no protocol limit on a
|
||||
*received* payload; 128 bytes is a local allocation choice only, and a receiver
|
||||
should tolerate more rather than reject.
|
||||
|
||||
---
|
||||
|
||||
### 19.11 Field Ordering Verification — RESOLVED
|
||||
@@ -344,22 +359,26 @@ allocate at least 128 bytes for the parameter buffer.
|
||||
|
||||
---
|
||||
|
||||
### 19.3 Server Close During Test — NOT DIRECTLY TESTED
|
||||
### 19.3 Server Close During Test — NOT DIRECTLY TESTED (superseded)
|
||||
|
||||
**Limitation:** Triggering a server-initiated close mid-test requires
|
||||
specific server conditions that are difficult to set up reliably in a
|
||||
**Limitation at the time:** Triggering a server-initiated close mid-test requires
|
||||
specific server conditions that were 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.
|
||||
reply) had not been verified externally, and was reclassified as **open /
|
||||
verification required**.
|
||||
|
||||
**Resolved in Part II by Finding S-4.** The maximum-test-duration hard limit was
|
||||
found to trigger it reliably, and the behavior was captured. This entry is
|
||||
retained only as a record of the earlier state.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
**Limitation:** How closely a client's own timestamp capture sits to its send
|
||||
and receive operations is internal to that client and cannot be verified through
|
||||
black-box testing, in either direction. Remains an implementation choice, and
|
||||
nothing normative depends on it.
|
||||
|
||||
---
|
||||
|
||||
@@ -497,8 +516,8 @@ 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
|
||||
(shown truncated)." Recomputing the HMAC from the captured bytes confirms that
|
||||
both 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.
|
||||
|
||||
@@ -560,6 +579,12 @@ irtt client -l 200 127.0.0.1 # request length 200
|
||||
|
||||
## Captures Created
|
||||
|
||||
All capture files referenced in this report were metadata-reviewed and their
|
||||
containers rewritten to strip capture comments, section and interface
|
||||
descriptions, host/user/machine identification, capture filter strings and
|
||||
filesystem paths. Packet bytes and timestamps were not altered — full frame
|
||||
hexdumps and per-frame epoch timestamps were verified identical before and after.
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `captures/full-session.pcapng` | Complete session: open, 3 echoes, close |
|
||||
@@ -608,3 +633,551 @@ See `test-vectors/` directory.
|
||||
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
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Part II — Server-Side Verification
|
||||
|
||||
**Date:** 2026-08-11
|
||||
**Baseline:** the upstream **0.9.1 release** (protocol version 1)
|
||||
**OS:** macOS Darwin 25.5.0 arm64 (Apple Silicon)
|
||||
|
||||
**Method, and what each part of it establishes:**
|
||||
|
||||
- **Black-box observation.** A raw UDP harness drove a real upstream server
|
||||
directly, so that requests no conforming client would emit could be tested. It
|
||||
shares no code with any implementation. Everything it measured is stated here
|
||||
as observed wire behavior.
|
||||
- **Black-box observation at the application layer.** The real upstream 0.9.1
|
||||
*client* was driven against various servers, and its externally visible
|
||||
reaction — what it recorded, what it reported, how it exited — was observed.
|
||||
The client is an independent second implementation for this purpose.
|
||||
- **Contaminated-side consistency validation.** A server was also written on the
|
||||
contaminated side from the behavioral model and driven by the real upstream
|
||||
client. Because it was produced on this side of the boundary it is **not** an
|
||||
independent implementation, and no statement in this report rests on it. What
|
||||
it provides is a consistency check: it demonstrates that the model as written
|
||||
is sufficient to drive a real upstream client. The *client's* reaction to it is
|
||||
black-box observation; the server itself is not.
|
||||
|
||||
Findings feed `IRTT_SERVER_PROTOCOL_SPEC.md`. Input → output tables are in
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md`.
|
||||
|
||||
---
|
||||
|
||||
## Part II Environment
|
||||
|
||||
```
|
||||
$ irtt version
|
||||
irtt version: 0.9.1
|
||||
protocol version: 1
|
||||
json format version: 1
|
||||
```
|
||||
|
||||
**Provenance of the second binary.** A second server binary was built from the
|
||||
upstream development tree as it stood **six upstream commits past the 0.9.1
|
||||
tag**, for the version-difference comparison in Finding S-13. Those six commits
|
||||
are post-release work and are **not** part of 0.9.1; every result attributed to
|
||||
that build is labelled as such and never folded into the 0.9.1 baseline.
|
||||
|
||||
*(An earlier revision of this report said "8 commits past the 0.9.1 tag". That
|
||||
count was wrong: it included two commits belonging to this clean-room project
|
||||
itself, which were present in the same working tree but are not upstream work.
|
||||
The upstream count is six.)*
|
||||
|
||||
---
|
||||
|
||||
## Finding S-1: Session identity is token + source endpoint
|
||||
|
||||
A post-open request is bound to a session by its token **and** by an exact match
|
||||
on the source address, source port, and address family.
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| Correct token, original endpoint | Served |
|
||||
| Correct token, different source port | **Dropped, no reply; session intact** |
|
||||
| Correct token, other address family's listener on the same port | Dropped |
|
||||
| Unknown token | Dropped |
|
||||
| Zero token | Dropped |
|
||||
| Token of a closed session | Dropped |
|
||||
| **Close** bearing a valid token from a foreign source port | **Dropped; session not closed** |
|
||||
|
||||
Endpoint binding protects the close path as well as the echo path. A capture is
|
||||
in `captures/server-session-identity.pcapng`.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-2: Received window resets to `0x1` on reordering and large gaps
|
||||
|
||||
The single most consequential server behavior found. A request arriving with a
|
||||
sequence number lower than the highest already seen, or 64 or more ahead of the
|
||||
previous one, causes the reported window to become `0x1` — history is discarded,
|
||||
not merely left unshifted, and it is never recovered.
|
||||
|
||||
```
|
||||
seq 0 -> count 1 window 0x0000000000000001
|
||||
seq 1 -> count 2 window 0x0000000000000003
|
||||
seq 5 -> count 3 window 0x0000000000000031
|
||||
seq 3 -> count 4 window 0x0000000000000001 <- gap fill discards history
|
||||
```
|
||||
|
||||
Duplicates leave the window unchanged and advance the count. 32-bit sequence
|
||||
wraparound is handled as ordinary modular arithmetic. Window value 0 was never
|
||||
observed. Full tables in `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1;
|
||||
capture in `captures/server-recv-window.pcapng`.
|
||||
|
||||
The three-case rule stated in `IRTT_SERVER_PROTOCOL_SPEC.md` Section 10.2 was
|
||||
cross-checked against a live server over 12 sessions of 40 requests each — 480
|
||||
replies — with sequence numbers drawn from a mix of in-order steps, duplicates,
|
||||
forward gaps of 2 to 80 and backward steps of 1 to 5, and starting sequence
|
||||
numbers of 0, 1, 7, 100 and 4294967294. **480 replies checked, 0 mismatches** in
|
||||
either window or count.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 12.8 and 19.15).
|
||||
|
||||
## Finding S-3: Received count semantics
|
||||
|
||||
The count advances by one for every request that reaches the reply stage,
|
||||
including duplicates and out-of-order arrivals, and never for requests dropped
|
||||
for bad magic, bad flags, bad authentication, unknown token, endpoint mismatch,
|
||||
oversize, or rate limiting. It is per session and includes the request being
|
||||
answered. Confirmed against a rate-limited server: 12 back-to-back requests
|
||||
produced 5 replies with counts 1–5, and three later requests continued 6–8.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-4: Server-initiated close exists and is triggered by the duration limit
|
||||
|
||||
Resolves client-spec open question 19.3. A server with a maximum test duration
|
||||
answers the first echo request past its hard deadline with a complete echo reply
|
||||
carrying flags `0x06` (Reply|Close), and removes the session at that moment.
|
||||
|
||||
| Configured maximum duration | Close flag observed at |
|
||||
|---|---|
|
||||
| 1 s | 3.01 s after the first echo |
|
||||
| 2 s | 4.06 s / 4.10 s after the first echo |
|
||||
|
||||
The deadline is the maximum duration plus a 2-second grace, measured from the
|
||||
session's first echo request — not from open. Capture:
|
||||
`captures/server-close.pcapng`, frame 36.
|
||||
|
||||
Driving an upstream 0.9.1 client against a server that sets the flag mid-test:
|
||||
the client discards that reply's measurement, terminates with a "server closed
|
||||
connection" condition, prints partial results, and exits successfully.
|
||||
|
||||
No other condition — idle expiry, shutdown, rate limiting, oversize, auth
|
||||
failure — was observed to set the flag.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 13.7 and 19.3).
|
||||
|
||||
## Finding S-5: Client-initiated close is never acknowledged, and a close reply is harmless
|
||||
|
||||
Upstream 0.9.1 sends **nothing** in response to a close request. Repeated closes
|
||||
and post-close echoes are dropped as unknown-token packets. Capture:
|
||||
`captures/server-close-lifecycle.pcapng` ends with three unanswered client
|
||||
packets.
|
||||
|
||||
A contaminated-side server that *does* reply to close — magic + flags(Reply|Close)
|
||||
+ [MAC] + token — was driven against a real 0.9.1 client and the run completed
|
||||
normally with correct results, with nothing in the client's output changing.
|
||||
Sending such a datagram is therefore harmless, but nothing was observed to make
|
||||
use of it: it has no demonstrated purpose in protocol version 1, and the server
|
||||
specification does not define it as required behavior.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-6: Protocol version is not enforced by the server
|
||||
|
||||
Open requests with `ProtocolVersion` of 0, 2, −1, or the tag omitted entirely,
|
||||
each produced a session-creating open reply (flags `0x03`, non-zero token) with
|
||||
`ProtocolVersion = 1` returned. The client spec's claim that "the server will
|
||||
also set the Close flag if there is a version mismatch" is not true of upstream
|
||||
0.9.1.
|
||||
|
||||
**Classification:** CLIENT SPEC CORRECTION (client spec Sections 6.2, 9.1,
|
||||
19.13).
|
||||
|
||||
## Finding S-7: Dual-field midpoint wire representation
|
||||
|
||||
With StampAt = midpoint and a single negotiated clock, the reply carries both
|
||||
midpoint fields — wall then monotonic — which is one 8-byte field more than the
|
||||
negotiated parameters imply, and makes the datagram longer at small negotiated
|
||||
lengths. Measured across every combination of received-stats and clock; confirmed
|
||||
at the application layer by an upstream client reporting 36-byte packets while
|
||||
receiving 44-byte datagrams and completing normally.
|
||||
|
||||
A contaminated-side server emitting only the negotiated midpoint field was driven
|
||||
by the 0.9.1 client for wall-only, monotonic-only and dual-clock configurations,
|
||||
and the client completed every run cleanly. This is consistency validation, not
|
||||
an independent observation; what it shows is that the *client* accepts the
|
||||
conforming form.
|
||||
|
||||
**Extended 2026-08-11 — see Finding S-18** for the negotiated-length dependence,
|
||||
the version comparison, and the correction to the "always +8" characterisation.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 8.5 and 19.14).
|
||||
|
||||
## Finding S-8: Interval restriction resolves the client's safety floor
|
||||
|
||||
A server restricts the negotiated interval to at most one quarter of its idle
|
||||
timeout (measured: 4 s → 1 s, 8 s → 2 s, 20 s → 5 s, 40 s → 10 s, 60 s → 15 s).
|
||||
This gives an ordinary, unmodified server that reduces a client's interval, which
|
||||
resolves client-spec open question 19.9.
|
||||
|
||||
| Server idle timeout | Returned interval | Client outcome |
|
||||
|---|---|---|
|
||||
| 2 s | 500 ms | Aborts — strict **and** loose |
|
||||
| 3 s | 750 ms | Aborts — strict **and** loose |
|
||||
| 4 s | 1 s (no reduction) | Proceeds |
|
||||
|
||||
Reduction to exactly 1 s is an ordinary restriction: rejected in strict mode,
|
||||
accepted in loose mode. The safety floor is **1 second**.
|
||||
|
||||
**Classification:** CLIENT SPEC CLARIFICATION (client spec Sections 6.2, 19.9).
|
||||
|
||||
## Finding S-9: Every rejection is a silent drop
|
||||
|
||||
No error, reset, or NACK exists anywhere in the protocol. Verified for: short
|
||||
datagrams, bad magic, reserved flag bits, the Reply flag on an inbound packet,
|
||||
every authentication failure mode, malformed parameters, invalid parameter enum
|
||||
values, unknown tokens, endpoint mismatch, oversize requests and rate limiting.
|
||||
Full table in `IRTT_SERVER_PROTOCOL_SPEC.md` Section 17.
|
||||
|
||||
Minimum accepted request sizes without authentication: open 4 bytes, close
|
||||
12 bytes, echo 16 bytes; add 16 bytes with authentication.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION, CONSISTENT with the client
|
||||
spec's HMAC-drop finding (19.8).
|
||||
|
||||
## Finding S-10: Authentication must be applied to replies, including echo replies
|
||||
|
||||
A contaminated-side server that authenticated its open reply correctly but
|
||||
omitted the HMAC flag from echo replies caused the 0.9.1 client to discard every
|
||||
echo reply and terminate abnormally with zero packets received. Fixing the flag
|
||||
made all configurations pass. The load-bearing observation here is the **client's**
|
||||
reaction, which is black-box; the server was merely the stimulus.
|
||||
|
||||
All authentication failures — missing flag, missing field, wrong key, flipped
|
||||
bit, zeroed MAC, truncated MAC, flag/MAC disagreement in either direction —
|
||||
produce a silent drop with no session state change. A session survived eight
|
||||
consecutive failing probes and served the next valid request normally.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-11: Idle expiry has a 5-second grace, and never-used sessions do not expire
|
||||
|
||||
With a 2-second idle timeout, the boundary was measured between 6.9 s and 7.1 s,
|
||||
i.e. timeout + 5 s. The first request past the deadline is still answered and
|
||||
the session is released while handling it; only the next request is dropped.
|
||||
|
||||
A session that has never carried an echo request does not expire at all: opened
|
||||
and left idle for 9 seconds under a 2-second timeout, it was served normally.
|
||||
Combined with the fact that every open — including a retransmitted one — creates
|
||||
a new session, this means open retransmissions accumulate permanent state.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-12: No session or per-peer limits by default
|
||||
|
||||
1000 sequential opens from a single source endpoint all succeeded with distinct
|
||||
tokens, and the first, middle and last were all still usable afterwards. Two
|
||||
opens from the same source port yield two independently usable sessions.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-13: Tested post-0.9.1 tree — CLI-only difference
|
||||
|
||||
The development tree built for this study is **six upstream commits** past the
|
||||
0.9.1 tag. Those commits are not part of the 0.9.1 release, and nothing in this
|
||||
finding describes 0.9.1. The only functional change among them is an opt-in
|
||||
server ECN option; the remainder are documentation, roadmap and dependency
|
||||
updates.
|
||||
|
||||
- The option's documentation states that it "disables UDP replies from server."
|
||||
**Observed behavior contradicts this**: with the option enabled, sessions
|
||||
completed normally over both IPv4 and IPv6 (4/4 packets, no loss).
|
||||
- Enabling it forces the source-IP-pinning policy.
|
||||
- ECN observation is a receive-side property of the IP header and does not
|
||||
change the IRTT payload in either direction.
|
||||
|
||||
A side-by-side run of both builds with an identical scripted request sequence
|
||||
(open with full statistics and dual timestamps on both clocks; echoes 0, 1, 4,
|
||||
3; close) produced identical structure: same open-reply layout, 60-byte replies,
|
||||
same flags, counts 1–4, windows `0x1`, `0x3`, `0x19`, `0x1`, and no close reply
|
||||
in either case.
|
||||
|
||||
**Classification:** VERSION DIFFERENCE — CLI / server-policy. Not wire-visible.
|
||||
|
||||
## Finding S-14: Reply length is capped by the interface MTU but the negotiated value is not
|
||||
|
||||
On a 1500-byte-MTU interface, negotiated lengths of 1501, 2000, 3000 and 8000
|
||||
each produced 1500-byte replies while the open reply returned the requested
|
||||
value unchanged. Clients treat a short reply as fatal, so such a session opens
|
||||
and then fails on the first echo.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-15: Robustness hazards reachable from ordinary traffic
|
||||
|
||||
Two conditions terminate the upstream server process. Both are recorded in
|
||||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 22 so that a clean implementation does
|
||||
not reproduce them.
|
||||
|
||||
1. **Oversized reply.** If the negotiated length exceeds the host's maximum
|
||||
outbound datagram size, the send fails with a non-transient error, which is
|
||||
treated as fatal: the listener stops and the process exits. Observed on a
|
||||
host with a 9216-byte maximum: a session negotiating 15000 bytes killed the
|
||||
server on its first echo reply. The default configuration places no upper
|
||||
bound on the negotiated length.
|
||||
2. **Zero-length fill pattern.** A server-fill descriptor of the form
|
||||
`pattern:` with an empty hex body terminates the server during open handling,
|
||||
when the fill allow-list permits pattern fills. The default allow-list does
|
||||
not, so a default-configured server is not exposed. An unparseable hex body
|
||||
such as `pattern:zz` is handled correctly by falling back to the server
|
||||
default.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-16: Server fill is a continuous stream
|
||||
|
||||
The default fill is the repeating pattern `69 72 74 74`, and its phase is not
|
||||
reset per packet or per session — it advances continuously as bytes are consumed
|
||||
and carries over between sessions on the same listener.
|
||||
|
||||
```
|
||||
seq 0 payload: 72 74 74 69 72 74 74
|
||||
seq 1 payload: 69 72 74 74 69 72 74
|
||||
seq 2 payload: 74 69 72 74 74 69 72
|
||||
seq 3 payload: 74 74 69 72 74 74 69
|
||||
```
|
||||
|
||||
Under a no-fill policy the payload contains unspecified residual bytes; one
|
||||
observed reply's trailing bytes were a fragment of a previous packet's parameter
|
||||
payload.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION; also noted as a client-side
|
||||
caution in client spec Section 8.5.
|
||||
|
||||
## Finding S-17: Contaminated-side consistency validation of the model
|
||||
|
||||
A server built from the behavioral model in `IRTT_SERVER_PROTOCOL_SPEC.md` — with
|
||||
no upstream code — was driven by the real 0.9.1 client across: default
|
||||
parameters; no statistics and no timestamps; midpoint with wall, monotonic and
|
||||
both clocks; send-only with wall; receive-only with monotonic; a 200-byte packet
|
||||
length; no-test mode; HMAC with a matching key; HMAC with a mismatched key; and
|
||||
IPv6. Every configuration produced a clean, lossless run with correct results,
|
||||
and the mismatched-key case correctly timed out.
|
||||
|
||||
**This is contaminated-side consistency validation, not independent
|
||||
verification.** The server was written on this side of the clean-room boundary,
|
||||
so it cannot corroborate anything about upstream. What it does show is that the
|
||||
specification as written is sufficient to build a server the real upstream client
|
||||
accepts — the client's acceptance being the black-box part. No statement anywhere
|
||||
in the outgoing documents rests on this exercise.
|
||||
|
||||
**Classification:** Consistency validation, not a finding.
|
||||
|
||||
---
|
||||
|
||||
## Part II Captures Created
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `captures/server-recv-window.pcapng` | Sequential, gap, duplicate, reorder and far-gap sequence numbers on one session |
|
||||
| `captures/server-close-lifecycle.pcapng` | Client close, repeated close, post-close echo — none answered |
|
||||
| `captures/server-close.pcapng` | Server-initiated close via the duration limit; frame 36 carries flags `0x06` |
|
||||
| `captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, original endpoint still served |
|
||||
|
||||
## Part II Vectors Created
|
||||
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md`.
|
||||
|
||||
## Part II Client Spec Changes Applied
|
||||
|
||||
| Section | Change | Class |
|
||||
|---------|--------|-------|
|
||||
| 2 | Pointer to the companion server specification | Editorial |
|
||||
| 6.2, 9.1, 19.13 | Server does not enforce the protocol version | **CORRECTION** |
|
||||
| 6.2, 19.9 | Interval safety floor pinned to 1 second, enforced in loose mode too | **CLARIFICATION** |
|
||||
| 8.5, 19.14 | The one verified case in which a reply exceeds `compatible_reply_len`, scoped to the single extra length `upstream_0_9_1_reply_len`; `compatible_reply_len` rather than the negotiated Length is the normal minimum; payload content is not predictable | **CLARIFICATION** |
|
||||
| 12.8, 19.15 | A window of `0x1` carries no history; window 0 is never emitted | **CLARIFICATION** |
|
||||
| 13.7, 19.3 | Server-initiated close confirmed; observed client reaction | **CLARIFICATION** |
|
||||
| 19.10 | No protocol limit on received parameter payload size | **CLARIFICATION** |
|
||||
|
||||
## Part II Summary
|
||||
|
||||
- Resolved client-spec open questions: **19.3**, **19.9**, **19.10**.
|
||||
- New client-spec open questions resolved on discovery: **19.13**, **19.14**,
|
||||
**19.15**.
|
||||
- Still unresolved on the client side: **19.6** (not externally testable),
|
||||
**19.12** (hard to trigger).
|
||||
- Server-side unknowns are enumerated in `IRTT_SERVER_PROTOCOL_SPEC.md`
|
||||
Section 23.
|
||||
|
||||
---
|
||||
|
||||
# Part III — Midpoint Timestamp Follow-Up (2026-08-11)
|
||||
|
||||
A focused follow-up on Finding S-7. Three server builds were exercised with a raw
|
||||
UDP harness that performs the open negotiation, sends one echo request, and
|
||||
reports the exact reply datagram length and the bytes at the timestamp offsets:
|
||||
|
||||
| Build | Provenance |
|
||||
|-------|-----------|
|
||||
| **0.9.1** | the upstream release; the baseline for this study |
|
||||
| **0.9.0** | the preceding upstream release, for comparison |
|
||||
| **post-0.9.1 tree** | the upstream development tree six upstream commits past the 0.9.1 tag — post-release work, not part of any release |
|
||||
|
||||
All on macOS arm64, loopback.
|
||||
|
||||
**Provenance note.** The measurements below are black-box observations. Part III
|
||||
additionally records one **source-assisted historical conclusion** — a judgement
|
||||
about whether the measured 0.9.0/0.9.1 difference was intended — which is
|
||||
labelled where it appears and is **not** presented as independently verified.
|
||||
Nothing normative in the specifications depends on it, and the detailed
|
||||
contaminated-side evidence behind it was retained on the contaminated side and
|
||||
does not appear here.
|
||||
|
||||
## Finding S-18: Dual-field midpoint emission is length-dependent in effect
|
||||
|
||||
### The wire representation
|
||||
|
||||
For StampAt = midpoint, upstream 0.9.1 emits two 8-byte timestamp fields — wall
|
||||
first, monotonic second — for every negotiated Clock, occupying a fixed 16-byte
|
||||
region. Full StampAt × Clock matrix at negotiated length 0, received-stats =
|
||||
both, against 0.9.1:
|
||||
|
||||
| StampAt | Clock = wall | Clock = monotonic | Clock = both |
|
||||
|---------|--------------|-------------------|--------------|
|
||||
| none | 28 | 28 | 28 |
|
||||
| send | 36 | 36 | 44 |
|
||||
| receive | 36 | 36 | 44 |
|
||||
| both | 44 | 44 | 60 |
|
||||
| **midpoint** | **44** (expected 36) | **44** (expected 36) | 44 |
|
||||
|
||||
Every non-midpoint row matches the negotiated layout exactly. Only midpoint
|
||||
deviates, and only when a single clock is negotiated.
|
||||
|
||||
### The length dependence — correction to "always +8"
|
||||
|
||||
Reply length is the larger of the negotiated length and the field block; the
|
||||
extra field enlarges only the field block. Writing `normal_header` for the block
|
||||
the negotiated parameters imply and `upstream_header` for that block plus one
|
||||
8-byte timestamp field:
|
||||
|
||||
```
|
||||
compatible_reply_len = max(negotiated_length, normal_header)
|
||||
upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)
|
||||
```
|
||||
|
||||
Negotiated-length sweep, 0.9.1, midpoint + wall + received-stats = both
|
||||
(`normal_header` 36, `upstream_header` 44):
|
||||
|
||||
| Negotiated length | 0 | 16 | 32 | 36 | 37 | 40 | 43 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Reply datagram | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
| Excess over a compatible server | +8 | +8 | +8 | +8 | +7 | +4 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Repeated for received-stats = none, count and window; the two minima shift to
|
||||
24/32, 28/36 and 32/40 and the pattern is identical. The dual-clock control case
|
||||
(midpoint + both clocks) showed zero excess at every length.
|
||||
|
||||
At negotiated length 4096 the midpoint region still contains both fields,
|
||||
followed by fill bytes — so the emission itself is unconditional; only its effect
|
||||
on the datagram length depends on the negotiated length.
|
||||
|
||||
**This corrects the earlier characterisation.** "Upstream replies are the
|
||||
negotiated length plus 8 bytes", or "the normal packet length plus 8", is true
|
||||
only while the negotiated length is at or below `normal_header`. Between the two
|
||||
minima the excess falls through +7…+1, and once the negotiated length reaches
|
||||
`upstream_header` the excess is **0** — the reply is exactly the negotiated
|
||||
length and the extra field displaces payload. Any surviving "+8 always" wording
|
||||
elsewhere is stale and wrong.
|
||||
|
||||
**And it is a per-negotiation figure.** Each column of the sweep is a distinct
|
||||
negotiation whose upstream reply has exactly one length,
|
||||
`upstream_0_9_1_reply_len`. The +8 / +7…+1 / 0 values are what the difference
|
||||
works out to across those different negotiations. No negotiation produced a range
|
||||
of lengths, and no intermediate length between `compatible_reply_len` and
|
||||
`upstream_0_9_1_reply_len` was ever observed. Wording that reads as "any reply up
|
||||
to 8 bytes over is acceptable" is equally stale and wrong.
|
||||
|
||||
### Reachable without requesting midpoint
|
||||
|
||||
Against a 0.9.1 server restricted to a single timestamp, a request for StampAt =
|
||||
both is answered with StampAt = midpoint (already recorded in the timestamp
|
||||
allowance table). Measured: requested `3/wall` → negotiated `4/wall`, reply 44
|
||||
where 36 was implied; requested `3/monotonic` → same; requested `3/both` →
|
||||
negotiated `4/both`, reply 44, no excess.
|
||||
|
||||
### Value consequences for a positional decoder
|
||||
|
||||
With Clock = monotonic the first 8 bytes of the midpoint region hold the wall
|
||||
value, so a decoder that expects one midpoint field reads a
|
||||
nanoseconds-since-epoch magnitude into its monotonic slot. Measured with the
|
||||
upstream 0.9.1 client itself (`--tstamp=midpoint --clock=monotonic`): reported
|
||||
server monotonic timestamps of 1786437872024631833 and 1786437872225745750 ns —
|
||||
about 56.6 years, not process uptime — with the run completing normally and
|
||||
reporting 72 bytes sent against 88 bytes received for two packets.
|
||||
|
||||
**Limit of what this supports.** The misread is verified; a general correction is
|
||||
not. Once the negotiated length reaches `upstream_header` the two forms are the
|
||||
same size:
|
||||
|
||||
```
|
||||
conforming single-clock: [mono][payload...]
|
||||
upstream 0.9.1: [wall][mono][payload...]
|
||||
```
|
||||
|
||||
and nothing in the datagram tells them apart. A client that unconditionally read
|
||||
the second 8-byte region as monotonic would, against a conforming server, be
|
||||
reading ordinary payload. The specifications therefore permit a correction only
|
||||
where the dual-field form is otherwise identifiable, and record the equal-length
|
||||
case as ambiguous rather than inventing a heuristic for it.
|
||||
|
||||
### Version comparison — black-box measured
|
||||
|
||||
Same harness, same parameters, negotiated length 0, received-stats = both:
|
||||
|
||||
| Build | midpoint + wall | midpoint + monotonic | midpoint + both |
|
||||
|-------|-----------------|----------------------|-----------------|
|
||||
| 0.9.0 | 36 bytes, one field | 36 bytes, one field, process-relative value | 44 bytes, two fields |
|
||||
| 0.9.1 | 44 bytes, two fields | 44 bytes, two fields | 44 bytes, two fields |
|
||||
| tested post-0.9.1 tree | 44 bytes, two fields | 44 bytes, two fields | 44 bytes, two fields |
|
||||
|
||||
0.9.0 emits exactly the negotiated fields; 0.9.1 emits the dual-field form; the
|
||||
tested post-0.9.1 tree behaves like 0.9.1. The full matrix and the length sweep
|
||||
were re-run against the post-0.9.1 build and matched 0.9.1 in every cell.
|
||||
|
||||
This is a measurement. It says which bytes each build put on the wire, and
|
||||
nothing about why.
|
||||
|
||||
### Deliberate or defect — source-assisted historical conclusion
|
||||
|
||||
**Labelled contaminated-side judgement. Not an independent observation, and not
|
||||
load-bearing.** Contaminated-side investigation of upstream's release history
|
||||
indicates that the 0.9.0-to-0.9.1 difference measured above is an **unintended
|
||||
regression** rather than a designed protocol feature. Nothing in upstream's
|
||||
published release notes or documentation refers to the behavior, and none of the
|
||||
six post-0.9.1 upstream commits changes it.
|
||||
|
||||
Three things about this paragraph:
|
||||
|
||||
1. The measured 0.9.0-versus-0.9.1 difference is stated separately, above, and
|
||||
stands entirely on its own.
|
||||
2. That measurement is *consistent with* this judgement but does not establish
|
||||
it. Intent is not observable from the wire.
|
||||
3. The contaminated-side evidence supporting the judgement was retained on the
|
||||
contaminated side and deliberately did not cross the boundary. Only the
|
||||
conclusion appears here.
|
||||
|
||||
No requirement in either specification depends on this paragraph. A reader who
|
||||
disregards it loses nothing normative.
|
||||
|
||||
**Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1,
|
||||
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
|
||||
19.14).
|
||||
@@ -1,8 +1,23 @@
|
||||
# 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
|
||||
- **Version:** 1.3-verified
|
||||
- **Date:** 2026-08-11
|
||||
- **Verified against:** irtt 0.9.1 release, protocol version 1, macOS Darwin
|
||||
25.3.0 / 25.5.0 arm64
|
||||
|
||||
**Changes in 1.2:** corrections and clarifications arising from server-side
|
||||
verification. See Sections 19.3, 19.9, 19.10, 19.13, 19.14 and 19.15, and the
|
||||
companion `IRTT_SERVER_PROTOCOL_SPEC.md`.
|
||||
|
||||
**Changes in 1.3:** the reply-length tolerance of Sections 8.5 and 19.14 is
|
||||
narrowed from "accept any longer reply" to the one verified single-clock midpoint
|
||||
case, and stated as a single additional accepted length —
|
||||
`upstream_0_9_1_reply_len` exactly — rather than a `+1..+8` range; the normal
|
||||
minimum and expected reply size is stated as `compatible_reply_len` rather than
|
||||
the negotiated `Length` value alone; the monotonic-only decoding ambiguity is
|
||||
recorded rather than papered over with a heuristic; the DSCP wire encoding is
|
||||
separated from user-facing notation (Section 10.8); and the stale suggestion that
|
||||
servers reject a protocol-version mismatch is removed (Section 6.2).
|
||||
|
||||
---
|
||||
|
||||
@@ -25,11 +40,12 @@ 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.
|
||||
server behavior is needed to understand client interoperability. Server
|
||||
behavior is specified separately in `IRTT_SERVER_PROTOCOL_SPEC.md`, which
|
||||
shares this document's wire format.
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -187,8 +203,11 @@ A client session progresses through the following externally observable states:
|
||||
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.
|
||||
Close in its request**: the server rejected the session. Transition to
|
||||
Failed. Note that upstream 0.9.1 servers were never observed to produce
|
||||
such a rejection — in particular they do **not** reject a mismatched
|
||||
protocol version this way (see below and Section 19.13) — so a client MUST
|
||||
handle the shape without depending on any server actually emitting it.
|
||||
- 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.
|
||||
@@ -203,12 +222,21 @@ A client session progresses through the following externally observable states:
|
||||
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).
|
||||
- Interval reduced below a safety floor of **1 second**. This rejection
|
||||
applies in loose mode as well as strict mode.
|
||||
[**Verified 2026-08-11** — see Section 19.9.]
|
||||
|
||||
- **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.
|
||||
|
||||
The client cannot rely on the server to detect the mismatch. Upstream 0.9.1
|
||||
servers do **not** reject a mismatched protocol version: they rewrite it to 1,
|
||||
open a normal session, and return `ProtocolVersion = 1` with the Close flag
|
||||
clear. Version checking is therefore effectively client-side only.
|
||||
[**Verified 2026-08-11** — see Section 19.13 and
|
||||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 7.5.]
|
||||
|
||||
### 6.3 Active Test
|
||||
|
||||
- **Entry:** Open reply received and parameters accepted.
|
||||
@@ -276,7 +304,7 @@ 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) |
|
||||
| Close Reply | Server → Client | Closing phase | No | Not part of normal operation. Upstream servers send nothing in response to a close; such a datagram is tolerated by clients but has no demonstrated purpose in protocol version 1 |
|
||||
|
||||
---
|
||||
|
||||
@@ -445,7 +473,103 @@ set (see Section 19.3 — not yet verified by black-box testing).
|
||||
- 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.
|
||||
**Payload:** Server-filled according to negotiation. The client MUST NOT rely on
|
||||
the payload's content: server fill is applied as a continuous stream, so a reply
|
||||
payload does not necessarily begin at the start of the configured fill pattern.
|
||||
|
||||
**Reply length:** The normal, expected size of an echo reply is
|
||||
`compatible_reply_len` — the larger of the negotiated Length and the mandatory
|
||||
field block, defined below. That, and not the negotiated `Length` value on its
|
||||
own, is the minimum: the client MUST reject a reply *shorter* than
|
||||
`compatible_reply_len`. (The two differ whenever the negotiated Length is smaller
|
||||
than the field block, including the common case of a negotiated Length of 0.)
|
||||
There is exactly one verified case in which a reply is *longer*, and the required
|
||||
tolerance is scoped to it — see below. This is **not** a general rule that
|
||||
overlong replies are acceptable; outside that one case, a client MUST keep
|
||||
applying its ordinary strict validation to unexpectedly sized datagrams.
|
||||
|
||||
**The one case: dual-field midpoint from upstream 0.9.1.** When midpoint
|
||||
timestamps are negotiated, upstream 0.9.1 servers emit **two** midpoint timestamp
|
||||
fields on the wire — the midpoint wall field followed by the midpoint monotonic
|
||||
field, in that order — **even when the negotiated clock selects only one clock**.
|
||||
A single-clock midpoint reply therefore carries one 8-byte field more than the
|
||||
negotiated parameters imply. (This describes bytes on the wire only; it implies
|
||||
nothing about how any server represents a midpoint internally.)
|
||||
|
||||
The extra field enlarges the reply's *minimum* size; it does **not** add 8 bytes
|
||||
to every reply. Writing `normal_header` for the field block the negotiated
|
||||
parameters imply and `upstream_header` for that block plus one 8-byte timestamp
|
||||
field:
|
||||
|
||||
```
|
||||
normal_header = negotiated field block
|
||||
upstream_header = normal_header + one 8-byte timestamp field
|
||||
|
||||
compatible_reply_len = max(negotiated_length, normal_header)
|
||||
upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)
|
||||
```
|
||||
|
||||
Three distinct quantities, not to be conflated: `negotiated_length` is the value
|
||||
of the negotiated Length parameter; `normal_header` is the negotiated mandatory
|
||||
field block; `compatible_reply_len` is the actual normal packet length implied by
|
||||
both.
|
||||
|
||||
So the excess is +8 while the negotiated length is at or below `normal_header`,
|
||||
shrinks through +7…+1 between the two minima, and is **0** once the negotiated
|
||||
length reaches `upstream_header` — there the extra field displaces 8 bytes of
|
||||
payload instead of lengthening the datagram. Those figures are values the
|
||||
difference takes **across different negotiations**. For any one set of negotiated
|
||||
Params both quantities are fixed numbers, and the upstream reply has exactly one
|
||||
length.
|
||||
|
||||
**Required tolerance.** When the negotiated parameters are StampAt = Midpoint
|
||||
with a single Clock, a conforming client MUST accept a reply whose length is
|
||||
**exactly** `upstream_0_9_1_reply_len`. It MUST NOT require the extra field to be
|
||||
present, and MUST NOT treat its presence as an error.
|
||||
|
||||
Where `upstream_0_9_1_reply_len > compatible_reply_len`, that form is identifiable
|
||||
by length alone and the difference between the two values is between 1 and 8
|
||||
bytes. This is one extra accepted length, **not** a `+1..+8` window: a client MUST
|
||||
NOT accept an arbitrary length in that span, and MUST reject any length strictly
|
||||
between `compatible_reply_len` and `upstream_0_9_1_reply_len` just as it rejects
|
||||
any other unexpected length.
|
||||
|
||||
**Accepted echo reply lengths, in full.** Compute both values once from the
|
||||
negotiated Params. A conforming client then accepts exactly these lengths:
|
||||
|
||||
| Received `packet_len` | Condition | Outcome |
|
||||
|---|---|---|
|
||||
| `== compatible_reply_len` | always | **accepted** — the normal exact form |
|
||||
| `== upstream_0_9_1_reply_len` | single-clock Midpoint **and** `upstream_0_9_1_reply_len > compatible_reply_len` | **accepted** — the identifiable upstream midpoint compatibility form |
|
||||
| any other value | — | **rejected** |
|
||||
|
||||
When `upstream_0_9_1_reply_len == compatible_reply_len` the two rows collapse: the
|
||||
packet is accepted at the normal exact length, the wire form is ambiguous, and no
|
||||
generic monotonic correction is applied (see below).
|
||||
|
||||
**Values read by a positional decoder.** The extra field is last, and field
|
||||
offsets are derived from the negotiated parameters:
|
||||
|
||||
- **Clock = wall:** the negotiated field comes first, so the value read is
|
||||
correct.
|
||||
- **Clock = monotonic:** the first 8-byte region holds the *wall*-domain value, so
|
||||
an ordinary positional decoder reads a nanoseconds-since-epoch magnitude into
|
||||
its monotonic slot instead of a process-relative one.
|
||||
|
||||
**Do not correct for that blindly.** Once the negotiated length reaches
|
||||
`upstream_header`, the upstream form and a conforming single-clock form produce
|
||||
datagrams of the same size, and nothing distinguishes them — the second 8-byte
|
||||
region is a timestamp under one form and ordinary payload under the other. A
|
||||
client MAY take its monotonic value from the second region **only when the
|
||||
dual-field form is otherwise identifiable** — that is, when
|
||||
`upstream_0_9_1_reply_len > compatible_reply_len` for the negotiated Params and
|
||||
the datagram length is exactly `upstream_0_9_1_reply_len`. In the equal-length
|
||||
regime (`upstream_0_9_1_reply_len == compatible_reply_len`) the value SHOULD
|
||||
be recorded as ambiguous rather than guessed. Either way the packet MUST still be
|
||||
accepted, and a client that applies no correction MUST NOT fail on the resulting
|
||||
implausible value. See the server specification, Section 11.3.1.
|
||||
[**Verified 2026-08-11**, extended **2026-08-11** with a negotiated-length sweep
|
||||
— see Section 19.14.]
|
||||
|
||||
### 8.5.1 Close Request Packet
|
||||
|
||||
@@ -505,8 +629,10 @@ 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).
|
||||
There is **no protocol limit** on the size of a received parameter payload; a
|
||||
receiver MUST tolerate a payload larger than any it would itself produce.
|
||||
Allocating at least 128 bytes for the parameter buffer is a safe local
|
||||
implementation choice, not a protocol rule (see Section 19.10).
|
||||
|
||||
**ReceivedStats enum values:**
|
||||
|
||||
@@ -548,7 +674,14 @@ 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.
|
||||
|
||||
**Do not depend on the server to signal the mismatch.** Upstream 0.9.1 servers
|
||||
accept any value in the `ProtocolVersion` parameter — including 0, 2, a negative
|
||||
value, or the tag being absent entirely — rewrite it to 1, create a normal
|
||||
session, and return an open reply with the Close flag clear and a non-zero
|
||||
token. A server MAY reject an unknown version by replying with the Close flag
|
||||
set and a zero token, but existing servers do not.
|
||||
[**Verified 2026-08-11** — see Section 19.13.]
|
||||
|
||||
### 9.2 Unknown Fields and Extensions
|
||||
|
||||
@@ -628,10 +761,18 @@ The following configuration parameters affect protocol behavior:
|
||||
- **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.]
|
||||
- **Wire encoding (protocol fact):** the DSCP parameter carries the **raw IP TOS
|
||||
/ Traffic Class byte**, range **0–255** — not a 6-bit codepoint. The
|
||||
differentiated-services codepoint occupies the upper 6 bits of that byte and
|
||||
the lower 2 (ECN) are typically zero, so Expedited Forwarding is **0xb8
|
||||
(184)** on the wire. Other common values: 0 (best effort), 0xa0 (CS5), 0x20
|
||||
(CS1). [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md
|
||||
Finding B.]
|
||||
- **Out of scope — user-facing notation.** Whether an implementation's command
|
||||
line and configuration accept a 0–63 codepoint (shifting it left by two to
|
||||
produce the wire byte) or the raw 0–255 byte is a presentation decision for
|
||||
that project, to be made and implemented separately. It is not a protocol
|
||||
fact and this specification does not prescribe it.
|
||||
- **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
|
||||
@@ -696,9 +837,14 @@ The following configuration parameters affect protocol behavior:
|
||||
- **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.
|
||||
- Receive: the server's arrival instant for the request.
|
||||
- Send: the server's departure instant for the reply.
|
||||
- Midpoint: the average of the two.
|
||||
|
||||
A client can rely on the pair bracketing the server's handling of that request,
|
||||
and on receive never being later than send. It cannot observe, and MUST NOT
|
||||
assume, anything about where a given server takes those readings internally or
|
||||
how much of its own send and receive path falls outside them.
|
||||
|
||||
### 11.2 Clock Types
|
||||
|
||||
@@ -857,22 +1003,46 @@ When the 64-bit received window is present in a reply:
|
||||
- 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.
|
||||
Bit 0 (LSB) is always set in a window emitted by a server, 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. Note that upstream 0.9.1
|
||||
servers never emit 0 when the window is negotiated at all.
|
||||
[**Verified 2026-08-11** — see Section 19.15.]
|
||||
|
||||
**A window of `0x1` carries no historical information.** Upstream 0.9.1 servers
|
||||
reset the window to `0x1` — not merely fail to update it — whenever a request
|
||||
arrives with a sequence number lower than the highest already seen, or with a
|
||||
gap of 64 or more from the previous request. `0x1` does **not** mean the previous
|
||||
63 packets were lost; it means no useful prior received-history is represented in
|
||||
the window after that transition. A single reordered packet discards everything
|
||||
the window knew about the previous 63 sequence numbers, and later packets rebuild
|
||||
it from scratch. A client that reads `0x1` literally will misclassify up to 63
|
||||
earlier packets as lost upstream after one reordering event.
|
||||
|
||||
A conforming client SHOULD treat a window value of `0x1` on any reply other than
|
||||
the first of the session as "no historical information available" rather than as
|
||||
"the server received nothing earlier."
|
||||
[**Verified 2026-08-11** — see Section 19.15 and
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1.]
|
||||
|
||||
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.
|
||||
- If the window cannot confirm (e.g., more than 64 packets ago, or the window
|
||||
carries no history) → generic loss.
|
||||
|
||||
The window has a maximum lookback of 63 packets (bits 1-63).
|
||||
|
||||
**Scope note.** The paragraphs above state what the window field *represents* on
|
||||
the wire, which is what was verified. The downstream/upstream loss
|
||||
classification built on top of it is a client-side interpretation question that
|
||||
has not been audited to the same standard here, and it is scheduled for its own
|
||||
review on the implementation side. Treat the classification rules as a starting
|
||||
point rather than as verified protocol semantics.
|
||||
|
||||
### 12.9 Statistics
|
||||
|
||||
For RTT, send delay, receive delay, and IPDV, a conforming client SHOULD
|
||||
@@ -935,13 +1105,24 @@ 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.
|
||||
A server MAY forcibly end a session mid-test by setting the Close flag (0x04) on
|
||||
an otherwise complete echo reply. This is the only in-band mechanism for a
|
||||
server-initiated close; there is no standalone server close packet.
|
||||
|
||||
**Note:** This behavior has not been directly verified by black-box
|
||||
testing (see Section 19.3). Implementations SHOULD handle it defensively.
|
||||
A client MUST handle this case. On receiving an echo reply with the Close flag
|
||||
set, the client MUST stop the test, MUST NOT send further test packets for that
|
||||
session, and SHOULD report the partial results gathered so far as a successful
|
||||
(if truncated) run rather than as an error.
|
||||
|
||||
Whether the close-flagged reply's own measurement is recorded is an
|
||||
implementation choice. Upstream 0.9.1 clients **discard** it: the run is
|
||||
terminated on the "server closed" condition before the measurement is recorded.
|
||||
|
||||
The session token is invalid from that moment; a close request the client sends
|
||||
afterwards will be silently dropped by the server, which is normal.
|
||||
|
||||
[**Verified 2026-08-11** — see Section 19.3 and
|
||||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2.]
|
||||
|
||||
---
|
||||
|
||||
@@ -962,7 +1143,8 @@ testing (see Section 19.3). Implementations SHOULD handle it defensively.
|
||||
| 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. |
|
||||
| Short reply (shorter than `compatible_reply_len`, the normal compatible reply length — not merely shorter than the negotiated Length) | SHOULD abort. |
|
||||
| Reply at an unexpected length (any length other than `compatible_reply_len`, or the single-clock midpoint form at exactly `upstream_0_9_1_reply_len`) | SHOULD abort. See Section 8.5. |
|
||||
| Stamp-at mismatch in reply | SHOULD abort. |
|
||||
| Clock mismatch in reply | SHOULD abort. |
|
||||
| Unexpected sequence number | SHOULD abort. |
|
||||
@@ -1331,18 +1513,32 @@ 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
|
||||
### 19.3 Server Close During Test — ✅ RESOLVED
|
||||
|
||||
**Status:** Not tested. Hard to trigger reliably with black-box testing.
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||
|
||||
**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?
|
||||
**Result:** Yes. A server that enforces a maximum test duration answers the
|
||||
first echo request arriving after its hard deadline (the configured maximum
|
||||
duration plus a 2-second grace, measured from the session's first echo request)
|
||||
with a complete, ordinary echo reply that additionally has the Close flag set,
|
||||
and removes the session at the same moment. Measured: a 1-second maximum
|
||||
produced a close-flagged reply 3.01 s after the first echo; a 2-second maximum
|
||||
produced one at 4.06 s.
|
||||
|
||||
**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.
|
||||
A conforming client never reaches this deadline, because it stops sending at the
|
||||
negotiated duration. The mechanism exists to bound sessions from clients that
|
||||
ignore the negotiation.
|
||||
|
||||
Observed upstream 0.9.1 client reaction: it discards the close-flagged reply's
|
||||
measurement, terminates the run with a "server closed connection" condition,
|
||||
prints the partial results, and exits with a success status.
|
||||
|
||||
No other server condition was observed to set the Close flag on an echo reply:
|
||||
idle expiry, shutdown, rate limiting, oversize requests and authentication
|
||||
failure were each exercised and none produced one.
|
||||
|
||||
See Section 13.7, `IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2, and
|
||||
`captures/server-close.pcapng` (frame 36 carries flags `0x06`).
|
||||
|
||||
### 19.4 Varint Encoding Compatibility — ✅ RESOLVED
|
||||
|
||||
@@ -1357,8 +1553,8 @@ verified. See BLACKBOX_VERIFICATION_REPORT.md Section 19.4.
|
||||
**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,
|
||||
payload) with the 16-byte HMAC field zeroed. Cross-checked with a separately
|
||||
written Python script 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.
|
||||
|
||||
@@ -1379,6 +1575,12 @@ 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.
|
||||
|
||||
**Extended 2026-08-11.** Upstream 0.9.1 was subsequently confirmed never to emit
|
||||
0 when the window is negotiated at all, and the far more common `0x1` was found
|
||||
to be a *valid* window carrying no history — which is a distinct condition from
|
||||
the 0 sentinel and must not be treated the same way. See Section 19.15 and
|
||||
Section 12.8.
|
||||
|
||||
### 19.8 Open Reply Without HMAC When Client Expects HMAC — ✅ RESOLVED
|
||||
|
||||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||||
@@ -1388,24 +1590,43 @@ 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
|
||||
### 19.9 Minimum Restricted Interval Safety Floor — ✅ RESOLVED
|
||||
|
||||
**Status:** Partially tested 2026-04-28.
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||
|
||||
**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.
|
||||
**Result:** The safety floor is **1 second**, and it is enforced in loose mode
|
||||
as well as strict mode.
|
||||
|
||||
### 19.10 Maximum Parameter Buffer Size — PARTIALLY RESOLVED
|
||||
The test uses an ordinary, unmodified server: a server restricts the negotiated
|
||||
interval to at most one quarter of its idle timeout, so an idle timeout below
|
||||
4 seconds forces a reduction below 1 second for a client requesting 1 second.
|
||||
|
||||
**Status:** Partially tested 2026-04-28.
|
||||
| Server idle timeout | Returned interval | Client outcome |
|
||||
|---------------------|-------------------|----------------|
|
||||
| 2 s | 500 ms | Aborts: reduction below 1 s — strict **and** loose |
|
||||
| 3 s | 750 ms | Aborts: reduction below 1 s — strict **and** loose |
|
||||
| 4 s | 1 s (no reduction for a 1 s request) | Proceeds |
|
||||
|
||||
**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.
|
||||
A reduction to exactly 1 second (requesting 2 s against a 4 s idle timeout) is
|
||||
treated as an ordinary server restriction: rejected in strict mode, accepted in
|
||||
loose mode.
|
||||
|
||||
See `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 4.4.
|
||||
|
||||
### 19.10 Maximum Parameter Buffer Size — ✅ RESOLVED (as a local choice)
|
||||
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||
|
||||
**Result:** There is no protocol limit on the size of a received parameter
|
||||
payload. An open request carrying over 200 bytes of parameters (80 repetitions
|
||||
of an unknown tag) was accepted and answered normally by an upstream server.
|
||||
|
||||
The largest payload a version-1 peer can legitimately *produce* remains small:
|
||||
the observed maximum from a real client was 52 bytes, and the theoretical
|
||||
maximum with all tags at their widest encodings plus a 32-byte ServerFill string
|
||||
is under 90 bytes. Allocating at least 128 bytes for the parameter buffer is a
|
||||
safe **implementation choice**, not a protocol requirement, and a receiver
|
||||
SHOULD tolerate larger payloads rather than rejecting them.
|
||||
|
||||
### 19.11 Field Ordering Verification — ✅ RESOLVED
|
||||
|
||||
@@ -1429,6 +1650,97 @@ negative value?
|
||||
This edge case requires server processing time to exceed the raw
|
||||
client-measured RTT, which is extremely unlikely under normal conditions.
|
||||
|
||||
### 19.13 Server-Side Protocol Version Enforcement — ✅ RESOLVED
|
||||
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||
|
||||
**Result:** Upstream servers do not enforce the protocol version. Open requests
|
||||
carrying `ProtocolVersion` values of 0, 2 and −1, and requests omitting the tag
|
||||
entirely, each produced a session-creating open reply with flags `0x03`
|
||||
(Open|Reply), a non-zero token, and `ProtocolVersion = 1` in the returned
|
||||
parameters.
|
||||
|
||||
**Consequence for the client:** the mismatch check in Sections 6.2 and 9.1 is
|
||||
the only line of defence and MUST be implemented. A client that assumes the
|
||||
server will reject it will silently run a test against an incompatible peer.
|
||||
|
||||
### 19.14 Reply Length Longer Than Negotiated — ✅ RESOLVED
|
||||
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64. Extended 2026-08-11
|
||||
with a negotiated-length sweep and a version comparison.
|
||||
|
||||
**Result:** Yes, in one specific, verified case. When midpoint timestamps are
|
||||
negotiated with a single clock (wall only or monotonic only), upstream 0.9.1
|
||||
servers emit both midpoint fields on the wire — wall field then monotonic field —
|
||||
which is one 8-byte field more than the negotiated parameters imply. Measured
|
||||
across all combinations of received-stats and clock with midpoint timestamps;
|
||||
confirmed at the application layer by an upstream client reporting 36-byte
|
||||
packets while receiving 44-byte datagrams, and completing normally.
|
||||
|
||||
**The excess is bounded by the ordinary length rule, not constant.** Reply length
|
||||
is the larger of the negotiated length and the field block; the extra field
|
||||
enlarges only the field block. Measured with received-stats = both, midpoint,
|
||||
wall (`normal_header` 36, `upstream_header` 44): negotiated lengths 0, 16, 32,
|
||||
36 → 44-byte replies; 37 → 44 (+7); 40 → 44 (+4); 43 → 44 (+1); 44 → 44 (0);
|
||||
45, 64, 1024, 4096 → exactly the negotiated length (0). At 4096 the midpoint
|
||||
region still contains both fields — the extra field displaces payload rather than
|
||||
extending the datagram. Any claim that upstream replies are simply "the
|
||||
negotiated length plus 8" is wrong outside the first regime. Each cell of that
|
||||
sweep is a different negotiation with a single reply length; the +7 / +4 / +1
|
||||
figures are differences between negotiations, never alternative lengths available
|
||||
within one.
|
||||
|
||||
**Scope of the requirement.** For single-clock midpoint the client MUST accept a
|
||||
reply at **exactly** `upstream_0_9_1_reply_len`. Where that value exceeds
|
||||
`compatible_reply_len` the form is identifiable by length alone and the two
|
||||
values differ by between 1 and 8 bytes — but that is one additional accepted
|
||||
length, not a `+1..+8` window, and a length strictly between the two MUST be
|
||||
rejected like any other unexpected length. That is the whole of the tolerance: it
|
||||
does **not** generalise to accepting arbitrary overlong echo replies, and the
|
||||
client MUST continue to reject replies *shorter* than `compatible_reply_len` —
|
||||
the normal compatible reply length, not merely the negotiated `Length` value —
|
||||
and to validate every other unexpectedly sized datagram strictly. It MUST NOT
|
||||
require the extra field to be present.
|
||||
|
||||
**The monotonic-only ambiguity.** Once the negotiated length reaches
|
||||
`upstream_header`, the upstream and conforming forms are the same size and are not
|
||||
distinguishable from the datagram alone. A client MUST NOT unconditionally read
|
||||
the second 8-byte region as the monotonic value whenever midpoint + monotonic is
|
||||
negotiated — against a conforming server that region is ordinary payload. Apply a
|
||||
correction only when the dual-field form is otherwise identifiable, meaning
|
||||
`upstream_0_9_1_reply_len > compatible_reply_len` and the datagram is exactly
|
||||
that length; otherwise record the value as ambiguous, and accept the packet
|
||||
either way. See the server specification, Section 11.3.1.
|
||||
|
||||
A server that emits only the negotiated fields is also fully interoperable: a
|
||||
server built to do so was driven by the upstream 0.9.1 client for wall-only,
|
||||
monotonic-only and dual-clock midpoint configurations and every run completed
|
||||
cleanly. Upstream 0.9.0 behaves that way on the wire; the dual-field form appears
|
||||
in 0.9.1 and is unchanged in the tested post-0.9.1 tree.
|
||||
|
||||
### 19.15 Received Window After Reordering or a Large Gap — ✅ RESOLVED
|
||||
|
||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||
|
||||
**Result:** The window is **reset to `0x1`**, not merely left unshifted, in both
|
||||
of these cases:
|
||||
|
||||
- a request whose sequence number is lower than the highest already seen
|
||||
(reordering, or a late gap fill);
|
||||
- a request whose sequence number is 64 or more ahead of the previous one.
|
||||
|
||||
The count still increments. A gap of exactly 63 preserves the older packet in
|
||||
bit 63; a gap of 64 discards everything.
|
||||
|
||||
The window value 0 was never observed; bit 0 is always set. 32-bit sequence
|
||||
wraparound is handled as ordinary modular arithmetic and does not disturb the
|
||||
window.
|
||||
|
||||
Full input → output tables are in
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1, with a capture in
|
||||
`captures/server-recv-window.pcapng`. See Section 12.8 for the classification
|
||||
consequence.
|
||||
|
||||
---
|
||||
|
||||
## 20. Non-Goals
|
||||
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -160,8 +160,8 @@ b9 07 05 06 06 06 07 06
|
||||
- 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).**
|
||||
**HMAC cross-checked with a separately written Python script (HMAC-MD5 over the
|
||||
entire packet with the HMAC field zeroed).**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,801 @@
|
||||
# IRTT Server Behavioral Vectors
|
||||
|
||||
**Source:** Black-box observation of an upstream IRTT **0.9.1 release** server on
|
||||
macOS Darwin 25.5.0 arm64, driven by a raw UDP harness.
|
||||
**Date:** 2026-08-11
|
||||
**Companion to:** `../IRTT_SERVER_PROTOCOL_SPEC.md`
|
||||
|
||||
These vectors are stated as **input → observed output**. They describe what a
|
||||
server was seen to emit for a given request sequence. They do not describe how
|
||||
any implementation produces those bytes.
|
||||
|
||||
Unless a row says otherwise, the server under test is the unmodified upstream
|
||||
0.9.1 release. Two other builds appear only in Section 5.2.3, each named
|
||||
explicitly: the 0.9.0 release, and a build of the upstream development tree six
|
||||
upstream commits past the 0.9.1 tag. Results from those builds never stand in for
|
||||
0.9.1.
|
||||
|
||||
Addresses in the accompanying captures are loopback only. Session tokens are
|
||||
server-assigned random values and differ per run; where a token appears below it
|
||||
is the value from that specific run and carries no meaning.
|
||||
|
||||
---
|
||||
|
||||
## 1. Received Window and Received Count
|
||||
|
||||
All vectors in this section were produced on a single session negotiated with
|
||||
ReceivedStats = both (3), StampAt = none (0). Each row is one echo request and
|
||||
the reply it produced. `count` is the 32-bit received count field; `window` is
|
||||
the 64-bit received window field.
|
||||
|
||||
### 1.1 Sequential, no loss
|
||||
|
||||
| # | request seq | reply count | reply window |
|
||||
|---|-------------|-------------|--------------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` |
|
||||
| 2 | 1 | 2 | `0x0000000000000003` |
|
||||
| 3 | 2 | 3 | `0x0000000000000007` |
|
||||
| 4 | 3 | 4 | `0x000000000000000f` |
|
||||
| 5 | 4 | 5 | `0x000000000000001f` |
|
||||
| 6 | 5 | 6 | `0x000000000000003f` |
|
||||
|
||||
### 1.2 One gap
|
||||
|
||||
| # | request seq | reply count | reply window | note |
|
||||
|---|-------------|-------------|--------------|------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` | |
|
||||
| 2 | 1 | 2 | `0x0000000000000003` | |
|
||||
| 3 | 3 | 3 | `0x000000000000000d` | bit 1 clear — sequence 2 missing |
|
||||
| 4 | 4 | 4 | `0x000000000000001b` | bit 2 clear — sequence 2 missing |
|
||||
|
||||
### 1.3 Multiple gaps
|
||||
|
||||
| # | request seq | reply count | reply window |
|
||||
|---|-------------|-------------|--------------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` |
|
||||
| 2 | 3 | 2 | `0x0000000000000009` |
|
||||
| 3 | 7 | 3 | `0x0000000000000091` |
|
||||
| 4 | 8 | 4 | `0x0000000000000123` |
|
||||
|
||||
### 1.4 Later gap fill — window collapses
|
||||
|
||||
| # | request seq | reply count | reply window | note |
|
||||
|---|-------------|-------------|--------------|------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` | |
|
||||
| 2 | 1 | 2 | `0x0000000000000003` | |
|
||||
| 3 | 5 | 3 | `0x0000000000000031` | gap over 2, 3, 4 |
|
||||
| 4 | 3 | 4 | **`0x0000000000000001`** | filling the gap **discards** history |
|
||||
|
||||
A late arrival does not set the corresponding historical bit. The window is
|
||||
reset to "only the current packet is known".
|
||||
|
||||
### 1.5 Duplicate sequence number
|
||||
|
||||
| # | request seq | reply count | reply window | note |
|
||||
|---|-------------|-------------|--------------|------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` | |
|
||||
| 2 | 1 | 2 | `0x0000000000000003` | |
|
||||
| 3 | 1 | 3 | `0x0000000000000003` | count advances, window unchanged |
|
||||
| 4 | 2 | 4 | `0x0000000000000007` | |
|
||||
|
||||
### 1.6 Reordering
|
||||
|
||||
| # | request seq | reply count | reply window | note |
|
||||
|---|-------------|-------------|--------------|------|
|
||||
| 1 | 0 | 1 | `0x0000000000000001` | |
|
||||
| 2 | 1 | 2 | `0x0000000000000003` | |
|
||||
| 3 | 2 | 3 | `0x0000000000000007` | |
|
||||
| 4 | 1 | 4 | **`0x0000000000000001`** | out-of-order arrival resets the window |
|
||||
| 5 | 3 | 5 | `0x0000000000000005` | rebuilds from the reset state |
|
||||
|
||||
Note row 5: the window says sequence 1 was received (bit 2) and sequence 3 was
|
||||
received (bit 0), but bit 1 is clear even though sequence 2 *was* received
|
||||
earlier. History before the reset is unrecoverable.
|
||||
|
||||
### 1.7 Strictly descending sequence numbers
|
||||
|
||||
| # | request seq | reply count | reply window |
|
||||
|---|-------------|-------------|--------------|
|
||||
| 1 | 5 | 1 | `0x0000000000000001` |
|
||||
| 2 | 4 | 2 | `0x0000000000000001` |
|
||||
| 3 | 3 | 3 | `0x0000000000000001` |
|
||||
| 4 | 2 | 4 | `0x0000000000000001` |
|
||||
| 5 | 1 | 5 | `0x0000000000000001` |
|
||||
| 6 | 0 | 6 | `0x0000000000000001` |
|
||||
|
||||
Every reply is answered; only the count carries information.
|
||||
|
||||
### 1.8 Gap at and beyond the representable window
|
||||
|
||||
| first seq | second seq | gap | second reply window |
|
||||
|-----------|------------|-----|---------------------|
|
||||
| 0 | 63 | 63 | `0x8000000000000001` — highest bit set |
|
||||
| 0 | 64 | 64 | `0x0000000000000001` |
|
||||
| 0 | 65 | 65 | `0x0000000000000001` |
|
||||
| 0 | 1000 | 1000 | `0x0000000000000001` |
|
||||
|
||||
The window represents the current packet plus 63 predecessors. A gap of exactly
|
||||
63 still records the older packet in the top bit; a gap of 64 or more discards
|
||||
all history.
|
||||
|
||||
### 1.9 Non-zero starting sequence number
|
||||
|
||||
| # | request seq | reply count | reply window |
|
||||
|---|-------------|-------------|--------------|
|
||||
| 1 | 100 | 1 | `0x0000000000000001` |
|
||||
| 2 | 101 | 2 | `0x0000000000000003` |
|
||||
| 3 | 102 | 3 | `0x0000000000000007` |
|
||||
|
||||
The first request of a session always yields window `0x1`, whatever its
|
||||
sequence number.
|
||||
|
||||
### 1.10 32-bit sequence wraparound
|
||||
|
||||
| # | request seq | reply count | reply window |
|
||||
|---|-------------|-------------|--------------|
|
||||
| 1 | 4294967293 | 1 | `0x0000000000000001` |
|
||||
| 2 | 4294967294 | 2 | `0x0000000000000003` |
|
||||
| 3 | 4294967295 | 3 | `0x0000000000000007` |
|
||||
| 4 | 0 | 4 | `0x000000000000000f` |
|
||||
| 5 | 1 | 5 | `0x000000000000001f` |
|
||||
|
||||
Wraparound is handled as ordinary modular arithmetic: crossing 2^32 − 1 → 0 is
|
||||
treated as a step of one, not as a gap of 2^32 − 1 and not as reordering.
|
||||
|
||||
Starting a session at 4294967295 and following with 0 gives `0x1` then `0x3`.
|
||||
|
||||
### 1.11 Randomized cross-check
|
||||
|
||||
The window and count rules in `../IRTT_SERVER_PROTOCOL_SPEC.md` Section 10 were
|
||||
checked against a live server over 12 sessions of 40 requests each — 480 replies
|
||||
in total — with sequence numbers drawn from a mix of in-order steps, duplicates,
|
||||
forward gaps of 2 to 80, and backward steps of 1 to 5, and with starting
|
||||
sequence numbers of 0, 1, 7, 100 and 4294967294.
|
||||
|
||||
**Result: 480 replies checked, 0 mismatches** in either the window value or the
|
||||
count value.
|
||||
|
||||
### 1.12 Count excludes dropped requests
|
||||
|
||||
Server configured with a minimum interval of 100 ms and a burst allowance of 5.
|
||||
Twelve requests sent back to back, then three more after a 600 ms pause.
|
||||
|
||||
| requests sent | replies received | counts in replies |
|
||||
|---------------|------------------|-------------------|
|
||||
| 12 back to back | 5 | 1, 2, 3, 4, 5 |
|
||||
| 3 after a pause | 3 | 6, 7, 8 |
|
||||
|
||||
Rate-limited requests produce no reply and do not advance the count.
|
||||
|
||||
---
|
||||
|
||||
## 2. Session Identity
|
||||
|
||||
Capture: `../captures/server-session-identity.pcapng`.
|
||||
|
||||
Session opened from source port A; token `88a76d2fd0cd5f32`.
|
||||
|
||||
| # | source | packet | observed |
|
||||
|---|--------|--------|----------|
|
||||
| 1 | A | open request | open reply, token assigned |
|
||||
| 2 | A | echo seq 0 | reply, count 1, window `0x1` |
|
||||
| 3 | **B** | echo seq 1, correct token | **no reply** |
|
||||
| 4 | **B** | close, correct token | **no reply**, session **not** closed |
|
||||
| 5 | A | echo seq 2 | reply, count 2, window `0x5` |
|
||||
| 6 | **C** | echo seq 0, token `1111111111111111` | **no reply** |
|
||||
| 7 | A | close | **no reply**, session removed |
|
||||
|
||||
Row 5 is the important one: the count advanced from 1 to 2, not to 3 — the
|
||||
foreign-endpoint request in row 3 was not counted. The window `0x5` reflects
|
||||
sequence 2 (bit 0) and sequence 0 (bit 2), with bit 1 clear because sequence 1
|
||||
never reached the session.
|
||||
|
||||
Row 4 confirms that a close from a foreign endpoint does not tear down the
|
||||
session.
|
||||
|
||||
Additional cases from the same class of experiment:
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| Two opens from the same source port | Two distinct tokens, both simultaneously usable |
|
||||
| Same token replayed to the other address family's listener on the same port | No reply |
|
||||
| Echo with an all-zero token | No reply |
|
||||
| Echo with the token of an already-closed session | No reply |
|
||||
| Repeated close with the same token | No reply |
|
||||
| 1000 opens from one source endpoint | 1000 distinct tokens, all usable |
|
||||
|
||||
---
|
||||
|
||||
## 3. Close Lifecycle
|
||||
|
||||
### 3.1 Client-initiated close
|
||||
|
||||
Capture: `../captures/server-close-lifecycle.pcapng`. Session token
|
||||
`eef1e3fb1ab2eb66`, negotiated ReceivedStats = both, StampAt = none.
|
||||
|
||||
| frame | direction | bytes |
|
||||
|-------|-----------|-------|
|
||||
| 1 | C → S | `14a75b01 01020280e0ba84bf030380a8d6b9070506` |
|
||||
| 2 | S → C | `14a75b03 eef1e3fb1ab2eb66 01020280e0ba84bf030380a8d6b9070506` |
|
||||
| 3 | C → S | `14a75b00 eef1e3fb1ab2eb66 00000000 00000000 0000000000000000` |
|
||||
| 4 | S → C | `14a75b02 eef1e3fb1ab2eb66 00000000 01000000 0100000000000000` |
|
||||
| 5 | C → S | `14a75b00 eef1e3fb1ab2eb66 01000000 00000000 0000000000000000` |
|
||||
| 6 | S → C | `14a75b02 eef1e3fb1ab2eb66 01000000 02000000 0300000000000000` |
|
||||
| 7 | C → S | `14a75b00 eef1e3fb1ab2eb66 02000000 00000000 0000000000000000` |
|
||||
| 8 | S → C | `14a75b02 eef1e3fb1ab2eb66 02000000 03000000 0700000000000000` |
|
||||
| 9 | C → S | `14a75b04 eef1e3fb1ab2eb66` — close request |
|
||||
| 10 | C → S | `14a75b04 eef1e3fb1ab2eb66` — repeated close |
|
||||
| 11 | C → S | `14a75b00 eef1e3fb1ab2eb66 09000000 …` — echo after close |
|
||||
|
||||
Frames 9, 10 and 11 receive **no response**. Frame 9 removes the session; 10 and
|
||||
11 are dropped as unknown-token packets. The capture ends with three unanswered
|
||||
client packets, which is the complete and correct close exchange for
|
||||
protocol version 1.
|
||||
|
||||
Close request layout: magic (3) + flags `0x04` (1) + token (8) = 12 bytes. With
|
||||
authentication: magic (3) + flags `0x0c` (1) + MAC (16) + token (8) = 28 bytes.
|
||||
|
||||
### 3.2 Server-initiated close
|
||||
|
||||
Capture: `../captures/server-close.pcapng`. Server configured with a maximum
|
||||
test duration of 2 s; client requested 600 s and was restricted to 2 s. Requests
|
||||
sent every ~250 ms.
|
||||
|
||||
| frame | direction | bytes (abridged) | note |
|
||||
|-------|-----------|------------------|------|
|
||||
| 2 | S → C | `14a75b03 f0592d9fb129558c 010202 80d0acf30e 03…` | duration restricted to 2 s |
|
||||
| 34 | S → C | `14a75b02 … 0f000000 10000000 ffff000000000000` | ordinary reply, seq 15 |
|
||||
| 35 | C → S | `14a75b00 … 10000000 …` | echo seq 16 |
|
||||
| **36** | S → C | `14a75b06 f0592d9fb129558c 10000000 11000000 ffff010000000000` | **flags `0x06` = Reply\|Close** |
|
||||
| 37 | C → S | `14a75b00 … 63000000 …` | echo seq 99 — **no reply** |
|
||||
|
||||
Frame 36 is a complete, ordinary echo reply — correct sequence number (16),
|
||||
correct token, count 17, window `0x1ffff` — with the Close flag additionally
|
||||
set. The session is gone from that moment; frame 37 is unanswered.
|
||||
|
||||
Timing measurements of the deadline, from the **first** echo request of the
|
||||
session:
|
||||
|
||||
| configured maximum duration | close flag observed at |
|
||||
|-----------------------------|------------------------|
|
||||
| 1 s | 3.01 s |
|
||||
| 2 s | 4.06 s |
|
||||
| 2 s (second run) | 4.10 s |
|
||||
|
||||
The deadline is the configured maximum duration plus a 2-second grace period.
|
||||
|
||||
### 3.3 Observed upstream client reaction to a close-flagged reply
|
||||
|
||||
Driving an upstream 0.9.1 client against a server that sets the Close flag from
|
||||
sequence 2 onward:
|
||||
|
||||
```
|
||||
seq=0 recorded
|
||||
seq=1 recorded
|
||||
seq=2 reply carries Close -> run terminates
|
||||
```
|
||||
|
||||
The client reports a "server closed connection" condition, prints the statistics
|
||||
for sequences 0 and 1 only — the close-flagged reply is **not** recorded — and
|
||||
exits with a success status.
|
||||
|
||||
---
|
||||
|
||||
## 4. Open Negotiation
|
||||
|
||||
### 4.1 Protocol version is not rejected
|
||||
|
||||
Server default configuration. Each row is an open request carrying only the
|
||||
listed ProtocolVersion value.
|
||||
|
||||
| requested ProtocolVersion | server response | reply parameter payload |
|
||||
|---------------------------|-----------------|-------------------------|
|
||||
| 1 | open reply, session created | `0102` (version 1) |
|
||||
| 0 | open reply, session created | `0102` (version 1) |
|
||||
| 2 | open reply, session created | `0102` (version 1) |
|
||||
| −1 | open reply, session created | `0102` (version 1) |
|
||||
| tag absent entirely | open reply, session created | `0102` (version 1) |
|
||||
|
||||
In every case the reply flags were `0x03` (Open\|Reply) with the Close flag
|
||||
**clear** and a non-zero token. Reply length 14 bytes: magic (3) + flags (1) +
|
||||
token (8) + `0102`.
|
||||
|
||||
### 4.2 Parameter payload acceptance
|
||||
|
||||
| open request parameter payload | response |
|
||||
|-------------------------------|----------|
|
||||
| empty | open reply, 14 bytes |
|
||||
| tag 2 only, no value bytes | no reply |
|
||||
| tag 2 with an incomplete multi-byte varint | no reply |
|
||||
| version 1 + duration 0 | no reply |
|
||||
| version 1 + duration −1 | no reply |
|
||||
| version 1 + interval −5 | no reply |
|
||||
| version 1 + ReceivedStats 4 | no reply |
|
||||
| version 1 + ReceivedStats −1 | no reply |
|
||||
| version 1 + StampAt 5 | no reply |
|
||||
| version 1 + StampAt 4 + Clock 3 | open reply |
|
||||
| version 1 + Clock 0 | no reply |
|
||||
| version 1 + Clock 4 | no reply |
|
||||
| version 1 + unknown tag 42 | open reply, tag ignored |
|
||||
| version 1 + unknown tag 200 | open reply, tag ignored |
|
||||
| version 1 + 80 repetitions of unknown tag 42 (~200 bytes) | open reply, all ignored |
|
||||
| version 1 + ServerFill declared 40 bytes | no reply |
|
||||
| version 1 + ServerFill declaring 10 bytes with only 2 present | no reply |
|
||||
| 50 bytes of arbitrary data | no reply |
|
||||
|
||||
### 4.3 No-test open
|
||||
|
||||
| direction | bytes |
|
||||
|-----------|-------|
|
||||
| C → S | `14a75b05` + parameter payload — flags Open\|Close |
|
||||
| S → C | `14a75b07 0000000000000000` + restricted parameters — flags Open\|Reply\|Close, **zero token** |
|
||||
|
||||
With authentication the reply flags are `0x0f` and the reply carries a valid MAC.
|
||||
The zero token is not usable: subsequent echo and close requests bearing it get
|
||||
no reply.
|
||||
|
||||
### 4.4 Interval restriction against the idle timeout
|
||||
|
||||
Client requests an interval of one hour; server has no configured minimum
|
||||
interval.
|
||||
|
||||
| server idle timeout | negotiated interval |
|
||||
|---------------------|---------------------|
|
||||
| 4 s | 1 s |
|
||||
| 8 s | 2 s |
|
||||
| 20 s | 5 s |
|
||||
| 40 s | 10 s |
|
||||
| 60 s | 15 s |
|
||||
|
||||
Client-side floor, measured with an upstream 0.9.1 client requesting a 1 s
|
||||
interval:
|
||||
|
||||
| server idle timeout | server-returned interval | client outcome |
|
||||
|---------------------|--------------------------|----------------|
|
||||
| 2 s | 500 ms | aborts — reduction below 1 s, in strict **and** loose mode |
|
||||
| 3 s | 750 ms | aborts — reduction below 1 s, in strict **and** loose mode |
|
||||
| 4 s | 1 s (no reduction) | proceeds |
|
||||
|
||||
Requesting a 2 s interval against a 4 s idle timeout produces a reduction to
|
||||
exactly 1 s: rejected in strict mode as an ordinary restriction, accepted in
|
||||
loose mode.
|
||||
|
||||
### 4.5 Timestamp allowance
|
||||
|
||||
Requested StampAt → negotiated StampAt, by server timestamp allowance:
|
||||
|
||||
| requested | dual | single | none |
|
||||
|-----------|------|--------|------|
|
||||
| 0 (none) | 0 | 0 | 0 |
|
||||
| 1 (send) | 1 | 1 | 0 |
|
||||
| 2 (receive) | 2 | 2 | 0 |
|
||||
| 3 (both) | 3 | **4** | 0 |
|
||||
| 4 (midpoint) | 4 | 4 | 0 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Reply Length
|
||||
|
||||
### 5.1 Length as a function of negotiated fields
|
||||
|
||||
Reply lengths, unauthenticated, measured across every combination. `S` is
|
||||
ReceivedStats, `A` is StampAt, `C` is Clock.
|
||||
|
||||
| S | A | C | reply length |
|
||||
|---|---|---|--------------|
|
||||
| 0 | 0 | any | 16 |
|
||||
| 1 | 0 | any | 20 |
|
||||
| 2 | 0 | any | 24 |
|
||||
| 3 | 0 | any | 28 |
|
||||
| 3 | 1 or 2 | 1 or 2 | 36 |
|
||||
| 3 | 1 or 2 | 3 | 44 |
|
||||
| 3 | 3 | 1 or 2 | 44 |
|
||||
| 3 | 3 | 3 | 60 |
|
||||
| 3 | 4 | 1 or 2 | **44** — see below |
|
||||
| 3 | 4 | 3 | 44 |
|
||||
|
||||
Add 16 bytes to every figure when authentication is in use.
|
||||
|
||||
### 5.2 Dual-field midpoint wire representation
|
||||
|
||||
For StampAt = 4 (midpoint), upstream 0.9.1 emits **two** 8-byte timestamp fields
|
||||
— the midpoint wall field followed by the midpoint monotonic field, in that order
|
||||
— for every negotiated Clock, occupying a fixed 16-byte region. With a single
|
||||
negotiated clock this is one field more than the negotiated parameters imply.
|
||||
These rows record bytes observed on the wire and nothing else. All vectors below
|
||||
at negotiated length 0.
|
||||
|
||||
| S | A | C | expected from negotiation | observed |
|
||||
|---|---|---|---------------------------|----------|
|
||||
| 0 | 4 | 1 | 24 | **32** |
|
||||
| 0 | 4 | 2 | 24 | **32** |
|
||||
| 0 | 4 | 3 | 32 | 32 |
|
||||
| 1 | 4 | 1 | 28 | **36** |
|
||||
| 1 | 4 | 2 | 28 | **36** |
|
||||
| 2 | 4 | 1 | 32 | **40** |
|
||||
| 2 | 4 | 2 | 32 | **40** |
|
||||
| 3 | 4 | 1 | 36 | **44** |
|
||||
| 3 | 4 | 2 | 36 | **44** |
|
||||
| 3 | 4 | 3 | 44 | 44 |
|
||||
|
||||
Raw reply for S=0, A=4, C=1 (wall only), 32 bytes:
|
||||
|
||||
```
|
||||
14 a7 5b 02 <token 8> 00 00 00 00
|
||||
a4 c1 74 90 47 83 ca 18 <- midpoint wall, 1786384545595310500
|
||||
da 45 a2 3b 00 00 00 00 <- midpoint monotonic, 1000490458
|
||||
```
|
||||
|
||||
The second field was not negotiated. An upstream 0.9.1 client run in this
|
||||
configuration reports "packet length: 36 bytes" while receiving 44-byte
|
||||
datagrams, and completes normally.
|
||||
|
||||
No other StampAt value deviates. Measured at negotiated length 0, S=3: `A=1` and
|
||||
`A=2` give 36 bytes for C=1 or C=2 and 44 for C=3; `A=3` gives 44 for C=1 or C=2
|
||||
and 60 for C=3 — all exactly as the negotiated layout implies.
|
||||
|
||||
### 5.2.1 The excess is bounded by the negotiated length
|
||||
|
||||
The extra field enlarges the field block, not every reply. Reply length remains
|
||||
the larger of the negotiated length and the field block (Section 5.3):
|
||||
|
||||
```
|
||||
normal_header = negotiated field block
|
||||
upstream_header = normal_header + one 8-byte timestamp field
|
||||
|
||||
compatible_reply_len = max(negotiated_length, normal_header)
|
||||
upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)
|
||||
```
|
||||
|
||||
Sweep for S=3, A=4, C=1 — `normal_header` 36, `upstream_header` 44:
|
||||
|
||||
| negotiated length | 0 | 16 | 32 | 36 | 37 | 40 | 43 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| reply datagram | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
| excess | +8 | +8 | +8 | +8 | +7 | +4 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Same pattern for the other received-stats settings, with the two minima at 24/32
|
||||
(S=0), 28/36 (S=1) and 32/40 (S=2). The C=3 control case shows zero excess at
|
||||
every negotiated length.
|
||||
|
||||
At negotiated length 4096 the midpoint region still holds both fields, followed
|
||||
by fill — the extra field displaces payload rather than extending the datagram.
|
||||
The excess is therefore **+8**, then **+7…+1**, then **0**, depending on the
|
||||
negotiated length. There is no negotiated length at which the reply is
|
||||
universally "negotiated length + 8".
|
||||
|
||||
Read that row across columns, not within a column. Each column is a separate
|
||||
negotiation whose upstream reply has exactly one length,
|
||||
`upstream_0_9_1_reply_len`. The +8 / +7…+1 / 0 figures are the values the
|
||||
difference takes across different negotiations; no single negotiation produces a
|
||||
range of lengths, and nothing here says an intermediate length between
|
||||
`compatible_reply_len` and `upstream_0_9_1_reply_len` was ever emitted or should
|
||||
be accepted.
|
||||
|
||||
**Consequence for decoding.** In the zero-excess regime
|
||||
(`upstream_0_9_1_reply_len == compatible_reply_len`) the upstream form
|
||||
`[wall][mono][payload…]` and a conforming single-clock form `[mono][payload…]`
|
||||
produce datagrams of identical length, and the vectors above provide no way to
|
||||
tell them apart. That ambiguity is recorded, not resolved.
|
||||
|
||||
### 5.2.2 Reached without requesting midpoint
|
||||
|
||||
Server restricted to a single timestamp, S=3, negotiated length 0:
|
||||
|
||||
| requested A/C | negotiated A/C | expected from negotiation | observed |
|
||||
|---------------|----------------|---------------------------|----------|
|
||||
| 3 / 1 | 4 / 1 | 36 | **44** |
|
||||
| 3 / 2 | 4 / 2 | 36 | **44** |
|
||||
| 3 / 3 | 4 / 3 | 44 | 44 |
|
||||
| 4 / 1 | 4 / 1 | 36 | **44** |
|
||||
|
||||
### 5.2.3 Version comparison
|
||||
|
||||
Negotiated length 0, S=3:
|
||||
|
||||
| build | A=4, C=1 | A=4, C=2 | A=4, C=3 |
|
||||
|-------|----------|----------|----------|
|
||||
| 0.9.0 release | 36, one field | 36, one field (process-relative value) | 44, two fields |
|
||||
| 0.9.1 release | **44, two fields** | **44, two fields** | 44, two fields |
|
||||
| tested post-0.9.1 tree (six upstream commits past the 0.9.1 tag) | **44, two fields** | **44, two fields** | 44, two fields |
|
||||
|
||||
The 0.9.0 release emits exactly the negotiated fields; 0.9.1 emits the dual-field
|
||||
form; the tested post-0.9.1 tree behaves like 0.9.1. The six post-0.9.1 commits
|
||||
are post-release work and are not part of the 0.9.1 release.
|
||||
|
||||
A contaminated-side server emitting **only** the negotiated midpoint field was
|
||||
driven by an upstream 0.9.1 client for wall-only, monotonic-only and dual-clock
|
||||
midpoint configurations, and the client completed every run cleanly. This is
|
||||
contaminated-side consistency validation; the black-box part of it is the
|
||||
client's acceptance of the conforming form.
|
||||
|
||||
### 5.3 Negotiated length versus actual reply length
|
||||
|
||||
Server bound to an interface with a 1500-byte MTU, ReceivedStats = none,
|
||||
StampAt = none (16-byte field block).
|
||||
|
||||
| requested length | negotiated length returned | reply datagram |
|
||||
|------------------|----------------------------|----------------|
|
||||
| 0 | 0 | 16 |
|
||||
| 10 | 10 | 16 |
|
||||
| 16 | 16 | 16 |
|
||||
| 17 | 17 | 17 |
|
||||
| 1000 | 1000 | 1000 |
|
||||
| 1400 | 1400 | 1400 |
|
||||
| 1500 | 1500 | 1500 |
|
||||
| 1501 | 1501 | **1500** |
|
||||
| 2000 | 2000 | **1500** |
|
||||
| 3000 | 3000 | **1500** |
|
||||
| 8000 | 8000 | **1500** |
|
||||
|
||||
The negotiated value returned to the client is not clamped; the datagram is.
|
||||
|
||||
A session negotiating a length of 0 or 20 with full statistics and dual
|
||||
timestamps on both clocks produced 60-byte replies — the field block wins when
|
||||
it is larger than the negotiated length.
|
||||
|
||||
### 5.4 Request length versus reply length
|
||||
|
||||
| negotiated length | request datagram | reply datagram |
|
||||
|-------------------|------------------|----------------|
|
||||
| 16 | 216 (200 trailing bytes) | 16 |
|
||||
| 64 | 16 | 64 |
|
||||
| 64 | 32 | 64 |
|
||||
| 64 | 64 | 64 |
|
||||
|
||||
The reply length depends only on the negotiation, never on the request.
|
||||
|
||||
### 5.5 Maximum request length policy
|
||||
|
||||
Server configured with a maximum packet length of 64; client requested 200 and
|
||||
was restricted to 64.
|
||||
|
||||
| request datagram | result |
|
||||
|------------------|--------|
|
||||
| 16 | reply, 64 bytes |
|
||||
| 32 | reply, 64 bytes |
|
||||
| 64 | reply, 64 bytes |
|
||||
| 65 | **no reply** |
|
||||
| 100 | **no reply** |
|
||||
| open request larger than 64 | **accepted**, open reply sent |
|
||||
|
||||
Server configured with a maximum packet length of 10 (below the minimum field
|
||||
block): the session opens and negotiates a length of 10, and every 16-byte echo
|
||||
request is dropped.
|
||||
|
||||
---
|
||||
|
||||
## 6. Server Fill
|
||||
|
||||
### 6.1 Default fill bytes and pattern phase
|
||||
|
||||
Default-configuration server, ReceivedStats = none, StampAt = none, so the
|
||||
payload begins at offset 16.
|
||||
|
||||
| negotiated length | reply payload |
|
||||
|-------------------|---------------|
|
||||
| 17 | `69` |
|
||||
| 20 | `72 74 74 69` |
|
||||
| 24 | `72 74 74 69 72 74 74 69` |
|
||||
| 48 | `72 74 74 69` repeated 8 times |
|
||||
|
||||
Four consecutive replies on one session with a 7-byte payload:
|
||||
|
||||
```
|
||||
seq 0: 72 74 74 69 72 74 74
|
||||
seq 1: 69 72 74 74 69 72 74
|
||||
seq 2: 74 69 72 74 74 69 72
|
||||
seq 3: 74 74 69 72 74 74 69
|
||||
```
|
||||
|
||||
The four-byte pattern `69 72 74 74` is emitted as a continuous stream; its phase
|
||||
is not reset per packet, and it carries over between sessions on the same
|
||||
listener.
|
||||
|
||||
### 6.2 Fill descriptor negotiation
|
||||
|
||||
Default allow-list (random fills only), negotiated length 32 so the payload is
|
||||
16 bytes:
|
||||
|
||||
| requested descriptor | negotiated descriptor returned | payload |
|
||||
|----------------------|-------------------------------|---------|
|
||||
| `rand` | `rand` | random, differs per packet |
|
||||
| `none` | `pattern:69727474` | `72747469…` |
|
||||
| `pattern:aabb` | `pattern:69727474` | `72747469…` |
|
||||
| `pattern:69727474` | `pattern:69727474` | `72747469…` |
|
||||
| `bogus` | `pattern:69727474` | `72747469…` |
|
||||
| 32-character string | `pattern:69727474` | `72747469…` |
|
||||
| 33-character string | — | **open dropped** |
|
||||
|
||||
Permissive allow-list:
|
||||
|
||||
| requested descriptor | negotiated descriptor returned | payload |
|
||||
|----------------------|-------------------------------|---------|
|
||||
| `rand` | `rand` | random |
|
||||
| `pattern:aabb` | `pattern:aabb` | `aabbaabbaabbaabb…` |
|
||||
| `pattern:00` | `pattern:00` | `0000000000000000…` |
|
||||
| `pattern:ff00` | `pattern:ff00` | `ff00ff00ff00ff00…` |
|
||||
| `pattern:zz` | `pattern:69727474` | `69727474…` (invalid hex, falls back) |
|
||||
| `none` | `none` | **unspecified residual bytes** |
|
||||
| `pattern:` | — | **server terminated** |
|
||||
|
||||
Empty allow-list: every requested descriptor, including `rand`, is answered with
|
||||
`pattern:69727474`.
|
||||
|
||||
### 6.3 The refused descriptor does not describe the payload
|
||||
|
||||
Two servers with different configured fills, default allow-list (`rand` only),
|
||||
negotiated length 32 so the payload is 16 bytes:
|
||||
|
||||
| Server fill | Requested descriptor | Negotiated descriptor returned | Payload |
|
||||
|-------------|----------------------|-------------------------------|---------|
|
||||
| random | none | absent | `25ae387a787d641d82eb514f2bfca15f` (random) |
|
||||
| random | `pattern:aabb` | `pattern:69727474` | `29ce866b6d192c74376e7161dd71975d` (random) |
|
||||
| random | `rand` | `rand` | random |
|
||||
| `pattern:abcd` | none | absent | `abcdabcdabcdabcdabcdabcdabcdabcd` |
|
||||
| `pattern:abcd` | `pattern:aabb` | `pattern:69727474` | `abcdabcdabcdabcdabcdabcdabcdabcd` |
|
||||
| `pattern:abcd` | `rand` | `rand` | random |
|
||||
|
||||
On refusal, the descriptor returned to the client is a **fixed default string**,
|
||||
not a description of the bytes the server actually sends. A client must not
|
||||
predict payload content from it.
|
||||
|
||||
### 6.4 No-fill payload content
|
||||
|
||||
Server configured with no fill and a permissive allow-list, negotiated length 32
|
||||
(16-byte payload):
|
||||
|
||||
| request payload | reply payload |
|
||||
|-----------------|---------------|
|
||||
| `000102…0f` (16 bytes) | `000102…0f` — the request's own bytes |
|
||||
| none (16-byte request) | `000102…0f` — **residue from the previous packet** |
|
||||
|
||||
On a `none` fill negotiated through the descriptor, one observed 16-byte payload
|
||||
was `c0cbacf6220380897a044009 046e6f6e`, whose trailing bytes are a fragment of
|
||||
the preceding open reply's parameter payload. Payload content under no-fill is
|
||||
unspecified and may contain data from unrelated traffic.
|
||||
|
||||
---
|
||||
|
||||
## 7. Authentication
|
||||
|
||||
Server configured with key `correctkey`.
|
||||
|
||||
| request | result |
|
||||
|---------|--------|
|
||||
| Open, flags `0x09`, valid MAC | Open reply, flags `0x0b`, valid MAC, 43 bytes |
|
||||
| Echo, flags `0x08`, valid MAC | Echo reply, flags `0x0a`, valid MAC, 44 bytes |
|
||||
| Close, flags `0x0c`, valid MAC | Session removed, **no reply** |
|
||||
| No-test open, flags `0x0d`, valid MAC | Reply flags `0x0f`, zero token, valid MAC |
|
||||
| Open with no HMAC flag and no MAC field | no reply |
|
||||
| Echo with no HMAC flag and no MAC field | no reply |
|
||||
| Close with no HMAC flag and no MAC field | no reply, **session survives** |
|
||||
| Open with MAC computed under a different key | no reply |
|
||||
| Echo with MAC computed under a different key | no reply |
|
||||
| Echo with one bit flipped in the MAC | no reply |
|
||||
| Echo with the MAC field all zeros | no reply |
|
||||
| Echo with the MAC field truncated to 8 bytes | no reply |
|
||||
| 19-byte datagram (too short for the MAC field) | no reply |
|
||||
| 20-byte datagram, flags `0x08`, valid MAC, nothing else | no reply |
|
||||
| Valid MAC but the HMAC flag cleared in flags | no reply |
|
||||
| MAC recomputed with the flag cleared, flag cleared | no reply |
|
||||
|
||||
Against a server with **no** key, any request with the HMAC flag set gets no
|
||||
reply.
|
||||
|
||||
**Session integrity across failures.** After the eight failing cases above were
|
||||
sent to a live session, the next correctly authenticated echo was served
|
||||
normally and the received count had advanced only for the accepted packets.
|
||||
|
||||
**Reply authentication is mandatory.** A contaminated-side server that
|
||||
authenticated its open reply correctly but omitted the HMAC flag from echo
|
||||
replies caused an upstream 0.9.1 client to discard every echo reply and terminate
|
||||
abnormally with zero packets received. The observation that matters is the
|
||||
*client's* reaction, which is black-box; the server was only the stimulus.
|
||||
|
||||
---
|
||||
|
||||
## 8. Idle Expiry
|
||||
|
||||
Server configured with a 2-second idle timeout. Each trial: open, one echo to
|
||||
start the idle clock, wait, then two echoes back to back.
|
||||
|
||||
| gap after the last accepted echo | first request after the gap | the request after that |
|
||||
|---|---|---|
|
||||
| 5.0 s | answered | answered |
|
||||
| 6.5 s | answered | answered |
|
||||
| 6.9 s | answered | answered |
|
||||
| 7.1 s | answered | **dropped** |
|
||||
| 7.5 s | answered | **dropped** |
|
||||
| 9.0 s | answered | **dropped** |
|
||||
|
||||
Deadline = idle timeout + 5 s. The first request past the deadline is still
|
||||
answered, and the session is released while handling it.
|
||||
|
||||
**A session that has never carried an echo request does not expire.** With the
|
||||
same 2-second timeout, a session left completely idle for 9 seconds after open
|
||||
was served normally on its first echo request.
|
||||
|
||||
With the idle timeout disabled, a session idle for 9 seconds was served normally
|
||||
and remained usable.
|
||||
|
||||
---
|
||||
|
||||
## 9. Malformed Packet Admission
|
||||
|
||||
Session established; probes sent on the same socket. Prefix used:
|
||||
`14a75b00` + token + `00000000`.
|
||||
|
||||
| probe | result |
|
||||
|-------|--------|
|
||||
| 0–3 byte datagram | no reply |
|
||||
| 4–11 byte datagram (flags `0x00`) | no reply |
|
||||
| 12–15 byte datagram (flags `0x00`) | no reply |
|
||||
| 16-byte datagram (flags `0x00`) | **reply, 28 bytes** |
|
||||
| magic `15a75b` | no reply |
|
||||
| magic `000000` | no reply |
|
||||
| flags `0x10`, `0x20`, `0x40`, `0x80`, `0xf0`, `0xff`, `0x1f` | no reply |
|
||||
| flags `0x02`, `0x03`, `0x06`, `0x07`, `0x0a` (Reply set) | no reply |
|
||||
| flags `0x09` (Open\|HMAC) to a non-authenticating server | no reply |
|
||||
| flags `0x01` (Open) | open reply, new session |
|
||||
| flags `0x05` (Open\|Close) | open reply, zero token |
|
||||
| echo-shaped datagram with flags `0x01` | treated as an open; the token bytes are parsed as parameters |
|
||||
| close with 20 trailing bytes | no reply, session removed |
|
||||
|
||||
A 12-byte datagram with flags `0x04` is a valid close request; a 12-byte
|
||||
datagram with flags `0x00` is too short to be an echo request and is dropped.
|
||||
|
||||
The Open flag decides *interpretation*, not admission. For an otherwise
|
||||
admissible datagram it wins over Close (`0x05` is the no-test open form) and over
|
||||
an echo-shaped body (the token/sequence bytes are read as Open parameter data).
|
||||
It does not rescue a datagram from an independent rejection: `0x03` (Open|Reply)
|
||||
is dropped for the Reply flag, `0x09` (Open|HMAC) to a non-authenticating server
|
||||
is dropped for the authentication mismatch, `0x1f` is dropped for the undefined
|
||||
flag bits, and an Open whose parameter payload is malformed or out of range is
|
||||
dropped as well (Section 4.2).
|
||||
|
||||
---
|
||||
|
||||
## 10. DSCP Negotiation and Application
|
||||
|
||||
The values below are **raw IP TOS / Traffic Class byte** values, which is what
|
||||
the wire parameter carries — not 6-bit codepoints. Expedited Forwarding is
|
||||
therefore 184 (0xb8) in this table, not 46.
|
||||
|
||||
| requested DSCP | negotiated value returned | echo replies |
|
||||
|----------------|---------------------------|--------------|
|
||||
| 0 | absent (0) | delivered |
|
||||
| 46 | 46 | delivered |
|
||||
| 184 | 184 | delivered |
|
||||
| 255 | 255 | delivered |
|
||||
| 256 | 256 | **all dropped** |
|
||||
| 300 | 300 | **all dropped** |
|
||||
| −5 | −5 | delivered |
|
||||
|
||||
With DSCP disallowed by server policy, every requested value is returned as
|
||||
absent (0) and replies are delivered normally.
|
||||
|
||||
A value the server cannot apply to its socket is still accepted during
|
||||
negotiation; the session then opens successfully and answers nothing.
|
||||
|
||||
---
|
||||
|
||||
## 11. Captures
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `../captures/server-recv-window.pcapng` | One session exercising sequential, gap, duplicate, reorder and far-gap sequence numbers |
|
||||
| `../captures/server-close-lifecycle.pcapng` | Client-initiated close, repeated close, and an echo after close — none answered |
|
||||
| `../captures/server-close.pcapng` | Server-initiated close via the maximum-duration limit; frame 36 carries flags `0x06` |
|
||||
| `../captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, and the original endpoint continuing to be served |
|
||||
|
||||
All captures are loopback traffic with the server on a fixed port and the client
|
||||
on an ephemeral port.
|
||||
|
||||
**Capture container metadata.** Every `.pcapng` under `../captures/` was reviewed
|
||||
and its container rewritten so that it carries no capture comment, no
|
||||
section/interface description, no host, user or capture-machine identification,
|
||||
no capture filter string, and no filesystem path — only the packet records and
|
||||
the rewriting tool's own version string. Packet bytes and timestamps were **not**
|
||||
altered: for each file, the full frame hexdump and the per-frame epoch timestamps
|
||||
were verified byte-identical before and after the rewrite.
|
||||
Reference in new issue
Block a user