docs(protocol): airlock server black-box findings #76

Merged
Lochnair merged 1 commits from docs/server-deep-dive-airlock into main 2026-08-12 09:40:57 +00:00
8 changed files with 1715 additions and 102 deletions
Showing only changes of commit 58472dbf7b - Show all commits

No files matched your search

+249 -6
View File
@@ -297,8 +297,9 @@ Two upstream robustness defects are recorded in the server specification
reachable from ordinary client traffic and a clean implementation has to decide reachable from ordinary client traffic and a clean implementation has to decide
what to do instead. They are stated as observed outcomes plus **robustness what to do instead. They are stated as observed outcomes plus **robustness
recommendations for the clean implementation** — explicitly not as recommendations for the clean implementation** — explicitly not as
interoperability requirements, since no conforming client can observe which interoperability requirements. A conforming client cannot trigger the
choice a server made — with no description of the internal cause. absent-Clock defect through conforming negotiation, but can observe a
process-wide outage if another peer triggers it; no internal cause is described.
### Server-Pass Sanitization Audit ### Server-Pass Sanitization Audit
@@ -463,15 +464,17 @@ and removed.
### Statement classification ### Statement classification
The outgoing documents now separate five kinds of statement and never conflate The outgoing documents now separate six kinds of statement and never conflate
them: interoperability requirement; black-box observed upstream behavior; them: interoperability requirement; black-box observed upstream behavior;
upstream policy or default; robustness recommendation for the clean black-box inference; upstream policy or default; robustness recommendation for
implementation; and source-assisted historical conclusion. the clean implementation; and source-assisted historical conclusion.
Specific reclassifications made in this pass: Specific reclassifications made in this pass:
- Surviving a failed reply send is a **robustness recommendation**, not a - Surviving a failed reply send is a **robustness recommendation**, not a
protocol MUST. No conforming client can observe which choice a server made. protocol MUST. A process-wide failure can be visible to unrelated conforming
clients, but that does not make a particular clean resilience policy an
interoperability requirement.
- Upstream's absence of session and per-peer bounds is **observed policy**, and - 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 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 should reproduce. The clean server is expected to be bounded; the shape of that
@@ -610,3 +613,243 @@ 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 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 2026-08-11: the dotted directory is the only ignored entry under `docs/`, and no
Finder metadata file exists anywhere in the outgoing tree. Finder metadata file exists anywhere in the outgoing tree.
---
## Server Deep-Dive Airlock (2026-08-12)
This section records **provenance and boundary discipline** for the material added
on 2026-08-12. It is not implementation archaeology, and it deliberately contains
no detail about what the contaminated-side work looked at.
### What happened outside this repository
A further source-assisted research phase on the upstream server took place in a
separate contaminated workspace, outside this repository and outside `docs/`. As
in the earlier passes, that workspace held upstream source, upstream tests,
upstream history, source-assisted analysis, purpose-built harnesses, build trees,
binaries, raw result files, server logs, and black-box packet evidence. The
overwhelming majority of it is not admissible here and did not cross.
### What was admitted, and what the airlock agent was allowed to see
The transfer was performed by an agent working in this clean repository under an
explicit airlock restriction. Its **entire** permitted view of the contaminated
workspace was:
1. a single sanitizer-candidate report of **independently black-box-verified**
findings — written to be transferable without upstream source access, and
containing no source snippets, filenames, function or type names, or internal
structure; and
2. the black-box packet captures that report referenced, for the purposes of
confirming the packet-level evidence, checking container metadata, and copying
suitable files into the clean evidence set.
The agent was explicitly **prohibited** from inspecting, and did not inspect:
- the source-assisted findings report;
- the unresolved/source question matrix;
- the contaminated workspace's own README;
- upstream source, upstream tests, or upstream git history;
- any upstream checkout;
- the research build trees and harnesses;
- raw result files;
- server logs.
It was further prohibited from searching the contaminated workspace at large. No
recursive search, and no history or blame inspection, was performed there; the
only accesses were direct reads of the two authorized items above. No raw result
file was opened at all — the authorized report's own text proved sufficient for
every statement transferred, so the narrowest available boundary was kept.
### What the boundary cost, deliberately
Several items in the authorized report were **not** transferred, and the reasons
are worth recording because they are the boundary doing its job:
- An account of the order in which the server reports rejection reasons, taken
from the server's own diagnostic output. Even though that output is externally
emitted, it describes internal check ordering, and the clean specification had
already deliberately replaced its admission narrative with a table of discard
conditions plus the two precedence relationships that were verified on the
wire. Re-importing an ordering narrative would have undone that work.
- A statement that a session is removed before its close-flagged reply is sent.
That is internal ordering; only its externally visible consequence crossed, and
that consequence was already recorded.
- A generalisation about which flag combinations are dropped on a data packet
carrying a live token. The clean documents already carry a **more precise**
account of the same behavior, and the broader statement would have weakened it.
- An inference that the wire format offers no general extension channel. It
crossed only as an explicitly labelled inference, alongside the byte encodings
actually tested, rather than as a property of the format.
Where the authorized report was insufficient to support a clean statement, the
statement was not made and the ambiguity was left standing. Nothing was resolved
by consulting anything else.
### Classification discipline
Every new statement was classified before it was written, and the classification
appears in the text: observed upstream behavior, black-box inference, upstream
policy, protocol requirement, robustness recommendation for the clean
implementation, or remaining unknown. The server specification's statement-level
table was extended with the inference level and with the host-specific marking
that several of these results require.
Two boundaries were held particularly deliberately:
- **An upstream defect was not promoted to a protocol requirement.** The most
significant finding of this pass is an open request that upstream accepts and
then faults on. It is recorded as an observed robustness defect with a
must-not-crash recommendation for the clean server, and the documents state
explicitly that reproducing the fault is not an interoperability requirement.
- **No implementation policy was chosen.** The documents record what upstream
does about an open that selects timestamps without a clock, about a negative
Length, about out-of-range DSCP values, and about payload fill — and in
each case say that the clean project's choice is a decision for the
implementation work, not one this evidence settles. Those decisions remain open.
Host-specific figures — interface MTUs, the tested host's maximum outbound
datagram size, interface-MTU effective-length decision knees, and platform
handling of out-of-range DSCP values — are labelled as host-specific wherever
they appear, so that none is mistaken for a protocol constant or compiled into an
implementation.
### Captures admitted
Three captures crossed, each because it materially supports a finding that is new
to the clean documents:
| File | Packets | What it proves |
|------|---------|----------------|
| `../protocol/captures/server-clock-absent.pcapng` | 3 | An open selecting timestamps with no Clock tag is answered with a token and no Clock parameter, and the first echo is the last packet in the exchange |
| `../protocol/captures/server-expiry-consumption.pcapng` | 14 | Control and foreign-endpoint-first test: whether the first tested packet to reach an expired session emits a reply |
| `../protocol/captures/server-dscp-interleaved.pcapng` | 32 | Four interleaved sessions with distinct DSCP values over three rounds, no cross-session leakage, unmarked open replies |
Each was reviewed before copying and rewritten into the container convention
already used here: packet records and the rewriting tool's own version string
only, with no capture comment, no section or interface description, no host, user
or machine identification, no capture filter, and no filesystem path. Packet
bytes and per-frame timestamps were **not** altered — for each file the full
frame hexdump and the per-frame epoch timestamps were verified identical before
and after, and the packet counts above are unchanged from the originals. The
copied files were re-inspected afterwards and contain no string other than the
rewriting tool's version.
Two further captures were reviewed and **not** admitted: one materially redundant
with a capture already present, and one that on inspection did not demonstrate
the behavior it was associated with. In both cases the textual result was kept
instead, which is the preferred trade — a textual black-box result is better than
an unnecessary addition to the evidence set. The reasoning is recorded in the
verification report rather than only here.
### Review corrections applied before merge
Review of the airlock commit found several places where the wording claimed more
than the admitted evidence supports. Each was narrowed to the observation, using
the clean evidence set only — no contaminated material was consulted to resolve
any of them:
- The reply-length cap is stated as byte-exact only for the two interfaces where
that was measurable. The loopback row shows lengths up to 8000 emitted
unclamped and nothing about a 16384-byte boundary, which this host cannot reach
because a reply beyond roughly 9300 bytes ends the server first.
- The interleaved DSCP capture is described as the three rounds it contains, and
the separate observation that an open reply is unmarked on a listener that has
already sent marked replies is attributed to the capture that actually shows it
— an existing capture from the first pass — rather than to the new one, whose
open exchanges all precede its first marked reply.
- The fill-phase result is scoped to continuity across the sessions tested and
across the tested IPv4 and IPv6 listeners, rather than asserting one stream
shared by every session of a process. The recommendation previously drawn from
it was **removed**: the default fill is a fixed public pattern, so its phase
carrying across sessions discloses nothing about another peer, and the
observation does not justify prescribing per-session fill state. The genuine
no-cross-peer-data invariant was already stated for the no-fill mode and is
untouched.
- The must-not-crash advice no longer uses RFC 2119 keywords, which this document
reserves for interoperability requirements. It is a robustness recommendation,
and how strictly the implementing project binds itself to it belongs in that
project's own guidance.
- The presence-versus-zero rule is scoped to the reply direction. Requests do
carry present zeros — `Length = 0` and `DSCP = 0` are accepted, and `Clock = 0`
is rejected precisely because it is present.
- The admission-table row for an open with no Clock now says the **open** is
answered normally, not the session, which is what the evidence shows.
- The maximum-duration origin refinement was carried into the client
specification as well, since both documents are compatibility baselines and
would otherwise state the lifecycle rule differently.
A second review round found five further places where the first correction pass
had left an inconsistency behind, and these were fixed the same way:
- The must-not-crash advice is called a **recommendation** everywhere, including
in the paragraph that follows it and in the behavioral vector, so that dropping
the RFC 2119 keywords was not undone two sentences later.
- The remaining-unknown entry on reply-length capping was brought into line with
the corrected §9.2: exact only for the Ethernet and tunnel interfaces, with the
loopback trial recorded as a lower bound.
- The maximum-duration origin is stated for the **two** drop classes actually
tested in the first-request position — oversized and foreign-endpoint — instead
of generalising to any dropped request. Corrected in the specification, the
vector and the verification report together.
- The "first post-deadline request is still answered" rule is qualified to a
request that would otherwise be served, since the same subsection then shows a
foreign-endpoint packet being dropped while still consuming the release.
- "Close packets are unmarked" is scoped to standalone datagrams. A
server-initiated close is an echo reply carrying the session's marking, and
both specifications now say so rather than leaving the two statements to
contradict each other.
A final documentation-only review correction restricted the maximum-duration
dropped-trigger result to the directly tested rate-limit case and corrected
fill alternatives as observable but interoperability-equivalent. It used no new
evidence or contaminated material.
None of these corrections required new evidence, and none changed a
classification from observation to requirement or the reverse.
No binaries, source, harnesses, build trees, server logs, result files or
research directories crossed.
### Clean-Room Compliance Checklist (Deep-Dive Airlock)
- [x] Source-assisted findings report never opened.
- [x] Unresolved/source question matrix never opened.
- [x] Upstream source, tests and history never opened.
- [x] Research build trees and harnesses never opened.
- [x] Raw result files never opened.
- [x] Server logs never opened.
- [x] No recursive search, log or blame inspection performed in the contaminated
workspace.
- [x] Only independently black-box-verified evidence transferred.
- [x] Every transferred statement classified, with observation separated from
inference and from recommendation.
- [x] Upstream defects recorded as defects, not as protocol requirements.
- [x] Host-specific figures labelled as host-specific.
- [x] Clean implementation policy decisions left open for the implementation work.
- [x] Captures metadata-reviewed, container-rewritten, and verified
byte-identical in packet data and timestamps.
- [x] No production code changed by the airlock work.
### 2026-08-12 — PR #76 final review wording cleanup
An interrupted prior agent's local maximum-duration and rate-limit corrections
were reviewed rather than discarded: the maximum-duration trigger clarification
was retained, while the rate-limit interaction was kept as a black-box inference
rather than promoted to unsupported RFC 2119 language. Using clean evidence only,
this review also removed the false request-length invariant, scoped normal DSCP
marking to `0..=255`, and clarified that a dropped first post-expiry packet can
release the session before any final reply is emitted. No contaminated material
was consulted.
### 2026-08-12 — PR #76 black-box claim review correction
The latest review found a blanket discard-state claim that conflicted with the
observed rate-limit idle refresh, stale compact maximum-duration and idle-expiry
summaries, and an over-prescriptive parameter-presence statement. It also found
two black-box observations expressed as internal mechanism claims: pre-policy
receive truncation and a rejected OPEN leaving no internal state; configured and
negotiated limiter intervals had likewise been conflated. The wording now states
observable results or explicit inference only. No contaminated material was
consulted, no new evidence was added, and no implementation policy was chosen.
+531 -7
View File
@@ -693,15 +693,18 @@ The upstream count is six.)*
A post-open request is bound to a session by its token **and** by an exact match 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. on the source address, source port, and address family.
The identity trials below used live, non-expired sessions. Finding S-21 records
the separately observed expired-session release case.
| Case | Result | | Case | Result |
|------|--------| |------|--------|
| Correct token, original endpoint | Served | | Correct token, original endpoint | Served |
| Correct token, different source port | **Dropped, no reply; session intact** | | Correct token, different source port | **Dropped, no reply; the live session remained usable from its original endpoint** |
| Correct token, other address family's listener on the same port | Dropped | | Correct token, other address family's listener on the same port | Dropped |
| Unknown token | Dropped | | Unknown token | Dropped |
| Zero token | Dropped | | Zero token | Dropped |
| Token of a closed session | Dropped | | Token of a closed session | Dropped |
| **Close** bearing a valid token from a foreign source port | **Dropped; session not closed** | | **Close** bearing a valid token from a foreign source port | **Dropped; does not close an otherwise live session** |
Endpoint binding protects the close path as well as the echo path. A capture is Endpoint binding protects the close path as well as the echo path. A capture is
in `captures/server-session-identity.pcapng`. in `captures/server-session-identity.pcapng`.
@@ -751,8 +754,9 @@ produced 5 replies with counts 1–5, and three later requests continued 6–8.
## Finding S-4: Server-initiated close exists and is triggered by the duration limit ## 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 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 answers the first otherwise-serviceable echo request past its hard deadline with
carrying flags `0x06` (Reply|Close), and removes the session at that moment. a complete echo reply carrying flags `0x06` (Reply|Close), and removes the
session at that moment.
| Configured maximum duration | Close flag observed at | | Configured maximum duration | Close flag observed at |
|---|---| |---|---|
@@ -760,7 +764,8 @@ carrying flags `0x06` (Reply|Close), and removes the session at that moment.
| 2 s | 4.06 s / 4.10 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 The deadline is the maximum duration plus a 2-second grace, measured from the
session's first echo request — not from open. Capture: session's first **served** echo — not from open. A deadline-crossing echo that is
itself rate-limited is dropped; the next served echo carries Close. Capture:
`captures/server-close.pcapng`, frame 36. `captures/server-close.pcapng`, frame 36.
Driving an upstream 0.9.1 client against a server that sets the flag mid-test: Driving an upstream 0.9.1 client against a server that sets the flag mid-test:
@@ -871,8 +876,9 @@ consecutive failing probes and served the next valid request normally.
## Finding S-11: Idle expiry has a 5-second grace, and never-used sessions do not expire ## 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, 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 i.e. timeout + 5 s. A post-deadline echo that would otherwise be served is
the session is released while handling it; only the next request is dropped. answered and releases the session while handling it; only the next request is
dropped.
A session that has never carried an echo request does not expire at all: opened 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. and left idle for 9 seconds under a 2-second timeout, it was served normally.
@@ -1181,3 +1187,521 @@ disregards it loses nothing normative.
**Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1, **Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1,
chatgpt-codex-connector[bot] commented 2026-08-12 08:00:17 +00:00 (Migrated from github.com)
Review

P2 Badge Do not narrow fatal configurations to two listener types

The experiments only establish this failure for the tested loopback/wildcard setup, so saying the reachable configurations are “exactly” loopback or wildcard overgeneralizes the evidence. A directly bound jumbo-MTU interface could also have a reply cap above the host's outbound datagram ceiling, and Section 23.5 explicitly leaves jumbo interfaces and other platforms untested; scope this to the observed configurations or state the actual condition that the reply cap exceeds the transmit ceiling.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Do not narrow fatal configurations to two listener types** The experiments only establish this failure for the tested loopback/wildcard setup, so saying the reachable configurations are “exactly” loopback or wildcard overgeneralizes the evidence. A directly bound jumbo-MTU interface could also have a reply cap above the host's outbound datagram ceiling, and Section 23.5 explicitly leaves jumbo interfaces and other platforms untested; scope this to the observed configurations or state the actual condition that the reply cap exceeds the transmit ceiling. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/47bf6a2f0f563ca91365a13c4b97a67f01246fb9/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:17:29 +00:00 (Migrated from github.com)
Review

Addressed in 064b7d550107c7e4d10cd96c93796c8d2731e07b. “Exactly loopback or wildcard” was removed. The text now scopes the observed failure to the tested configurations and the effective reply-cap versus host transmit-ceiling condition; jumbo and other untested interfaces remain unresolved.

Addressed in 064b7d550107c7e4d10cd96c93796c8d2731e07b. “Exactly loopback or wildcard” was removed. The text now scopes the observed failure to the tested configurations and the effective reply-cap versus host transmit-ceiling condition; jumbo and other untested interfaces remain unresolved.
chatgpt-codex-connector[bot] commented 2026-08-12 08:52:29 +00:00 (Migrated from github.com)
Review

P2 Badge Limit the MTU-cap claim to measured interfaces

Fresh evidence beyond the earlier specification fix is that this S-25 subheading remains unqualified: it says replies are capped at the bound interface's MTU, while the loopback result immediately below explicitly did not reach or observe its 16384-byte cap, and other platforms remain untested. Scope this heading to the measured Ethernet and tunnel interfaces, or describe only the observed effective boundary, so the evidence report does not present an unresolved host-specific rule as fact.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Limit the MTU-cap claim to measured interfaces** Fresh evidence beyond the earlier specification fix is that this S-25 subheading remains unqualified: it says replies are capped at the bound interface's MTU, while the loopback result immediately below explicitly did not reach or observe its 16384-byte cap, and other platforms remain untested. Scope this heading to the measured Ethernet and tunnel interfaces, or describe only the observed effective boundary, so the evidence report does not present an unresolved host-specific rule as fact. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/221c38e166983b8c6ba691e163030986ad0ce1d6/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
chatgpt-codex-connector[bot] commented 2026-08-12 08:52:29 +00:00 (Migrated from github.com)
Review

P2 Badge Qualify the drop-class heading as tested

The body limits this comparison to the enumerated classes (Every class tested), and the companion specification and vectors consistently call rate limiting the only tested drop class with this effect. Dropping that qualifier in the finding title turns the admitted comparison into a universal claim about all discard paths; retain “tested” here so unmeasured cases remain explicitly ambiguous.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Qualify the drop-class heading as tested** The body limits this comparison to the enumerated classes (`Every class tested`), and the companion specification and vectors consistently call rate limiting the only **tested** drop class with this effect. Dropping that qualifier in the finding title turns the admitted comparison into a universal claim about all discard paths; retain “tested” here so unmeasured cases remain explicitly ambiguous. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/221c38e166983b8c6ba691e163030986ad0ce1d6/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:55:03 +00:00 (Migrated from github.com)
Review

Agreed — the heading outran its own body. Now reads "On the two interfaces where the cap was reachable, the emitted reply is capped at the bind interface's MTU", leaving the loopback row as the lower bound it is (lengths to 8000 unclamped; ~9300 ends the process before 16384 can be observed).
🤖 Addressed by Claude Code

Agreed — the heading outran its own body. Now reads "On the two interfaces where the cap was reachable, the emitted reply is capped at the bind interface's MTU", leaving the loopback row as the lower bound it is (lengths to 8000 unclamped; ~9300 ends the process before 16384 can be observed). _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Lochnair commented 2026-08-12 08:55:04 +00:00 (Migrated from github.com)
Review

Agreed — the title was the only place missing the qualifier the body, the spec and the vectors all carry. Finding S-20 is now "the only tested drop class that keeps a session alive".
🤖 Addressed by Claude Code

Agreed — the title was the only place missing the qualifier the body, the spec and the vectors all carry. Finding S-20 is now "the only **tested** drop class that keeps a session alive". _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] commented 2026-08-12 09:01:37 +00:00 (Migrated from github.com)
Review

P2 Badge Limit constant margins to the measured settings

The sweeps cover only idle timeouts of 500 ms, 2 s, and 4 s and maximum durations of 500 ms, 1 s, and 3 s, so they establish constant margins across those tested settings—not across the entire configurable value domain. Calling both margins constant across “the configured value” promotes a three-point observation into an unrestricted rule; qualify the conclusion to the measured values or label the broader extrapolation as an inference/remaining ambiguity.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Limit constant margins to the measured settings** The sweeps cover only idle timeouts of 500 ms, 2 s, and 4 s and maximum durations of 500 ms, 1 s, and 3 s, so they establish constant margins across those tested settings—not across the entire configurable value domain. Calling both margins constant across “the configured value” promotes a three-point observation into an unrestricted rule; qualify the conclusion to the measured values or label the broader extrapolation as an inference/remaining ambiguity. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/3f02372f5bb3b55c726ecda7863f04c344838157/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 09:04:21 +00:00 (Migrated from github.com)
Review

Agreed — three swept points each. Finding S-22 now names the settings, says the sweeps rule out a margin proportional to the configured value over the range tested (which is what "additive" means here), and records that a threshold or different behavior outside that range would not have shown up and stays untested.
🤖 Addressed by Claude Code

Agreed — three swept points each. Finding S-22 now names the settings, says the sweeps rule out a margin proportional to the configured value over the range tested (which is what "additive" means here), and records that a threshold or different behavior outside that range would not have shown up and stays untested. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] commented 2026-08-12 09:11:25 +00:00 (Migrated from github.com)
Review

P2 Badge Remove the unobservable expiry-boundary count claim

In this trial the foreign-endpoint packet receives no reply and the session is already unavailable when every later probe arrives, so no emitted statistic reveals whether that packet changed the received count before release. The earlier live-session endpoint test does not establish counter handling on this distinct expired-session path; retain the observed drop and release outcome, but leave whether it was internally counted ambiguous.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Remove the unobservable expiry-boundary count claim** In this trial the foreign-endpoint packet receives no reply and the session is already unavailable when every later probe arrives, so no emitted statistic reveals whether that packet changed the received count before release. The earlier live-session endpoint test does not establish counter handling on this distinct expired-session path; retain the observed drop and release outcome, but leave whether it was internally counted ambiguous. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/7f5d2525201b8c3ff35ea4cbc7526639a6e43401/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 09:14:06 +00:00 (Migrated from github.com)
Review

Agreed — the session emitted no further reply, so no statistic can report whether that packet advanced the count. Finding S-1's not-counted result was measured on a live session where a later reply exposed the count, and I was carrying it onto a path where it does not apply. The report and the vector now state the release outcome and leave the counting question open; spec §18.2 already avoided the claim.
🤖 Addressed by Claude Code

Agreed — the session emitted no further reply, so no statistic can report whether that packet advanced the count. Finding S-1's not-counted result was measured on a live session where a later reply exposed the count, and I was carrying it onto a path where it does not apply. The report and the vector now state the release outcome and leave the counting question open; spec §18.2 already avoided the claim. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
chatgpt-codex-connector[bot] commented 2026-08-12 09:19:47 +00:00 (Migrated from github.com)
Review

P2 Badge Mark close-reply DSCP behavior as unverified

When a server-initiated Close is sent for a session with nonzero DSCP, the recorded evidence does not establish that it retains the mark: server-dscp-interleaved.pcapng contains only ordinary echo replies, while server-close.pcapng uses TOS 0 throughout. Calling the Close reply an ordinary marked echo assumes that setting the flag cannot affect marking; either document an observed nonzero-DSCP Close exchange or leave this behavior ambiguous rather than making it normative elsewhere.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Mark close-reply DSCP behavior as unverified** When a server-initiated Close is sent for a session with nonzero DSCP, the recorded evidence does not establish that it retains the mark: `server-dscp-interleaved.pcapng` contains only ordinary echo replies, while `server-close.pcapng` uses TOS 0 throughout. Calling the Close reply an ordinary marked echo assumes that setting the flag cannot affect marking; either document an observed nonzero-DSCP Close exchange or leave this behavior ambiguous rather than making it normative elsewhere. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/629ead0832e2d7b9c48a7f33f6523bd5672db08e/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 09:22:52 +00:00 (Migrated from github.com)
Review

Confirmed by checking every admitted capture — none contains a Close-flagged reply with nonzero TOS, and server-close.pcapng is TOS 0 throughout because that session negotiated no DSCP. The marking is a measured result, so I kept it but dropped the "ordinary marked echo reply that also sets the Close flag" clause (it argued from the flag rather than reporting the observation) and both tables now state that no capture shows it. Nothing normative depended on it: §15.2 and §24 make no marking claim.
🤖 Addressed by Claude Code

Confirmed by checking every admitted capture — none contains a Close-flagged reply with nonzero TOS, and `server-close.pcapng` is TOS 0 throughout because that session negotiated no DSCP. The marking is a measured result, so I kept it but dropped the "ordinary marked echo reply that also sets the Close flag" clause (it argued from the flag rather than reporting the observation) and both tables now state that no capture shows it. Nothing normative depended on it: §15.2 and §24 make no marking claim. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5, 21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
19.14). 19.14).
---
# Part IV — Server Deep-Dive Black-Box Pass (2026-08-12)
A further black-box pass over the server, aimed at behavior that only a
non-conforming peer can reach: parameter presence, the lifetime boundaries, the
drop classes, reply sizing, and IP marking. It resolves several statements that
earlier parts left thin, and it records one previously undocumented upstream
robustness defect that is reachable from two unauthenticated datagrams.
## Part IV Method and Environment
**Builds exercised**, all unmodified, all run as ordinary server processes with
documented command-line options only:
| Build | Role in this part |
|-------|-------------------|
| **0.9.1 release** | the baseline; every unqualified statement refers to it |
| **0.9.0 release** | reproduction check |
| **development tree, six upstream commits past the 0.9.1 tag** | reproduction check; post-release work, not part of any release |
**Platform:** macOS Darwin 25.5.0 arm64. Loopback except where an Ethernet or
tunnel interface is named.
**Instrument.** An independent raw-UDP harness sharing no code with any IRTT
implementation. It performs the open negotiation itself, emits exact request
bytes, and records exact reply bytes and monotonic arrival times. For IP marking
it reads the received TOS / traffic-class byte from the kernel.
**Server provenance.** Each server under test is an unmodified build of the
revision named in the table above — the 0.9.0 release, the 0.9.1 release, or the
development tree six upstream commits past the 0.9.1 tag. No source change,
patch, instrumentation or test hook was applied to any of them, and each was run
as an ordinary server process configured only through documented command-line
options.
**What counts as evidence here.** Request bytes, reply bytes, the presence or
absence of a reply, the reply's IP header marking, arrival times, and whether a
server process was still serving afterwards. Timing boundaries were established
by sweeping probe offsets, not by reading any value out of anything. Where a
result depends on the tested host — interface MTUs, the maximum outbound datagram
size, socket handling of out-of-range values — it is labelled host-specific below
and in the specification.
**Repeatability.** Timing boundaries were swept at 50 ms spacing with three
repetitions per configured value. Behavioral results are single-shot unless a
repetition count is given.
## Finding S-19: An open whose restricted StampAt remains non-none without a Clock is accepted, then fatal
The most serious result of this pass.
When timestamp restriction leaves the restricted (effective) `StampAt` non-none,
an open request carrying `StampAt` = 1, 2, 3 or 4 and **no Clock tag at all** is
accepted: the server returns an ordinary session-creating reply with a valid
non-zero token, and the reply carries no Clock parameter — none is synthesized.
The **first** echo request on that session receives no reply and the server
process terminates. On a wildcard bind, the listener for the other address family
dies with it, so unrelated sessions stop being served at the same moment.
```
C -> S 14a75b01 05 06 06 06 04 00
open; ReceivedStats=3, StampAt=3 (both), Length=0, no Clock tag
S -> C 14a75b03 7ca44127b2cd7eab 01 02 05 06 06 06
open reply; non-zero token, ProtocolVersion 1, ReceivedStats 3,
StampAt 3 — no Clock parameter
C -> S 14a75b00 7ca44127b2cd7eab 00000000
first echo, sequence 0 -> no reply; server no longer serving
```
| Input | Outcome |
|-------|---------|
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; restricted StampAt remains non-none | open accepted, first echo fatal |
| Requested StampAt 0, Clock tag absent | open accepted, serves normally, no timestamps |
| StampAt 3, **explicit `Clock = 0`** | **open dropped** — no reply, no session |
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; no-timestamp policy restricts StampAt to none | open accepted, serves normally, no timestamps |
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; single-timestamp policy leaves restricted StampAt non-none | open accepted, first echo still fatal |
Reproduced for all four non-none StampAt values on **0.9.0, 0.9.1 and the
development build**. Capture: `captures/server-clock-absent.pcapng` (3 packets).
The absent-versus-explicit-zero contrast is the load-bearing part: the two inputs
differ by one encoded tag and produce opposite outcomes, so parameter *presence*
is externally meaningful in this upstream behavior. An implementation that
chooses to reproduce those distinct outcomes needs request validation that can
distinguish the two forms; this observation does not prescribe a clean server's
representation or policy for adversarial inputs.
Nothing here describes why the server stops. That is not observable from the
wire, it is not stated, and no conclusion in this report depends on it.
**Classification:** SERVER-ONLY NEW INFORMATION — upstream robustness defect
(server spec Sections 7.2, 22.5, 24.3; vectors Section 4.7).
## Finding S-20: A rate-limited request is the only tested drop class that keeps a session alive
For each drop class: open a session, send one echo, inject the packet under test,
send another echo, and read the second reply's count and window. Separate timing
trials established whether the injected packet moved the expiry boundary.
Every class tested advanced neither the received count nor the received window.
They divide on the second question:
| Injected packet | Extends the observed idle lifetime |
|-----------------|------------------------------------|
| Bad magic, invalid flag bits, Reply/Open flag on a data packet | no |
| Unknown token, zero token | no |
| Valid token from a foreign source endpoint | no (but see S-21) |
| Truncated echo, oversized echo | no |
| Bad, missing, zeroed or truncated MAC | no |
| **Request arriving with no rate allowance left** | **yes** |
The rate-limit result was separated from its alternatives three ways rather than
asserted from one trial: a session fed *only* too-fast requests outlived its idle
deadline; a session fed the same cadence of oversized requests expired on
schedule; and a silent session expired on schedule. A two-probe method was used
at the boundary so that "still alive" is distinguished from "expired but not yet
touched" — the distinction S-21 turns on. Reproduced on all three builds.
This is a statement about how long the session kept being served. What a server
records when it declines a request is not observable and is not claimed.
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens the previously
unelaborated statement in server spec Section 18.2 (now Sections 9.3, 18.2;
vectors Section 8.2).
## Finding S-21: The first packet to release an expired session determines whether a final reply is emitted
Part II established that a post-deadline echo that would otherwise be served is
answered and releases the session while handling it. This pass establishes that
a packet which would normally be dropped can instead release the session without
receiving a reply, so no later request receives a final reply.
Both trials are in one capture, `captures/server-expiry-consumption.pcapng`
(14 packets), with a 3-second idle timeout — an 8-second effective boundary:
| Trial | Sequence | Client's first post-deadline echo |
|-------|----------|-----------------------------------|
| Control | nothing between the deadline and the client's request | **answered** (count 2); the next one dropped |
| Test | an echo bearing the correct token from a **foreign source port** arrived 0.3 s earlier, just past the same deadline | **no reply**, and none for the one after it |
The foreign-endpoint packet was dropped, as such a packet is at any time, and it
released the session without receiving a reply. Whether it advanced the session's
received count before the release is **not** observable here: no reply was ever
emitted for that session again, so no statistic carries the answer. Finding S-1's
result that a foreign-endpoint packet is not counted was measured on a **live**
session, where a later reply exposed the count, and it does not transfer to this
expired-session path. The same
release-without-a-final-reply outcome was produced by a truncated echo, an
oversized echo, a close from a foreign endpoint, and a burst of unrelated
ordinary opens.
Two scope limits are stated deliberately. These are the classes that were tested,
and the effect is not claimed for every possible packet. And nothing here
describes the order in which a server performs any internal step: what was
observed is that the session was gone when the legitimate request arrived, and
that in the control, with no intervening packet, it was not.
Separately, a burst of unrelated **ordinary opens** released an expired session
with no packet bearing its token involved at all, while a burst of **no-test**
opens did not — consistent with a no-test open creating no session.
**Classification:** SERVER-ONLY NEW INFORMATION (server spec Sections 16, 18.2;
vectors Section 8.3).
## Finding S-22: Both lifetime margins are fixed additive constants
Part II measured the idle grace at one timeout and the maximum-duration grace at
two maxima. Sweeping both across several configured values shows each margin is a
fixed addition, not a proportion, and not an artifact of when the session began.
Idle session-release sweep, with gaps measured from the last accepted echo. Each
probe used a first otherwise-serviceable echo after the gap and an immediate
follow-up; the boundary is inferred from whether that follow-up was dropped.
| Configured idle timeout | Longest gap with follow-up served | Shortest gap with follow-up dropped |
|-------------------------|----------------------------------|------------------------------------|
| 500 ms | 5.45 s | 5.50 s |
| 2 s | 6.95 s | 7.00 s |
| 4 s | 8.95 s | 9.00 s |
At and beyond the observed release boundary, the first otherwise-serviceable
post-gap echo may still receive a final lazy-release reply; the immediate
follow-up is then dropped. These values are not first-request rejection
thresholds.
Maximum duration, offsets measured from the first **served** echo:
| Configured maximum duration | Last offset answered normally | First offset carrying Close |
|-----------------------------|-------------------------------|-----------------------------|
| 500 ms | 2.45 s | 2.50 s |
| 1 s | 2.95 s | 3.00 s |
| 3 s | 4.95 s | 5.00 s |
Across the settings swept — idle timeouts of 500 ms, 2 s and 4 s, and maximum
durations of 500 ms, 1 s and 3 s — both margins held at five seconds and two
seconds respectively, to within the 50 ms probe spacing, and reproduced on all
three builds. The idle boundary did not shift with when the session was opened.
Each margin rests on three measured points. That is enough to rule out a margin
proportional to the configured value over the range tested, and it is what the
"additive" description means here. It does not establish the margin for every
configurable value: a threshold, a cap, or different behavior at much larger or
much smaller settings would not have shown up in these sweeps and remains
untested.
These are measured properties of the tested builds, stated with their tolerance.
They are upstream policy, not protocol constants, and no requirement in either
specification depends on either figure.
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens Findings S-4 and
S-11 (server spec Sections 15.2, 18.2, 19; vectors Sections 3.2, 8.1).
## Finding S-23: Maximum duration starts at the first served echo and closes on the first later served echo
The boundary sweep establishes two distinct points. The **origin** is the first
echo request that is actually **served**. The **trigger** is the first echo after
the resulting deadline that would otherwise be served.
| Case | Result |
|------|--------|
| Session opened 5 s before its first echo | deadline unchanged — the open does not start the clock |
| First request oversized, dropped by the maximum-length policy | clock not started; a later served echo starts it |
| First request from a foreign source endpoint, dropped | clock not started; a later served echo starts it |
| Echo that crosses the deadline | full normal reply with Close set, and **counted** in the statistics it reports |
| Deadline-crossing echo that is itself rate-limited | dropped; the **next served** echo carries the Close flag |
| Client's next echo, and its close, after the flag | both unanswered — the token is released |
Two drop classes were tested in the first-request position — oversized and
foreign-endpoint — and neither started the clock. That is the extent of the
result: the remaining drop classes were not exercised in that position, and no
general "any dropped request" rule is claimed. The rate-limit row is a different
question, about a request crossing an already-running deadline rather than one
that would start it.
The close-flagged reply itself is in `captures/server-close.pcapng`, frame 36,
from Part II; the boundary sweep is a timing measurement with no capture of its
own.
**Classification:** SERVER-ONLY NEW INFORMATION; refines Finding S-4 (server spec
Section 15.2; vectors Section 3.2).
## Finding S-24: Repeated tags — last valid wins; invalid cases are limited
A tag may appear more than once in one open payload. Among individually valid
occurrences the **last** takes effect; this was confirmed for ProtocolVersion,
Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP and ServerFill.
| Payload | Result |
|---------|--------|
| Duration 1 s, then 2 s | Duration 2 s |
| Duration 2 s, then 1 s | Duration 1 s |
| Clock 1, then 3 | Clock 3 |
| Clock 3, then 4 (out of range) | **no reply** |
| Duration 1 s, then 0 (invalid) | **no reply** |
| Duration 0 (invalid), then 1 s | **no reply** |
| ServerFill `rand`, then a 33-character string | **no reply** |
**Invalid repeated values were tested more narrowly.** An invalid Duration zero
occurrence was dropped in both tested positions: valid then invalid, and invalid
then valid. The Clock and ServerFill rows each tested an invalid second occurrence
and were also dropped. The opposite ordering was not tested for Clock or
ServerFill, and invalid duplicate occurrences were not tested for
ProtocolVersion, Interval, Length, ReceivedStats, StampAt or DSCP. These results
therefore do not establish a parser-wide, position-independent invalid-duplicate
rule for every parameter.
No conforming client emits a repeated tag; the result is recorded because a
decoder that stops at the first occurrence, or validates only the occurrence it
keeps, can diverge on the measured adversarial inputs.
Unknown tags were re-tested at 0, 10, 11, 100, 127, 128, 1000 and 2³². Each was
ignored with the rest of the payload still parsed, and an open carrying only
unknown tags created an ordinary default session. The value of an unknown tag is
consumed as a varint; no length-prefixed or string-shaped value form was accepted
for one.
Finally, a rejected open did not poison the next open: after each malformed
payload — incomplete trailing varint, tag with no value, length-prefixed string
running past the buffer, varint overflow — a well-formed open from the same
endpoint immediately afterwards was served normally. This establishes no
persistent externally visible effect on the following request; it does not
establish whether hidden accounting or temporary state exists.
**Classification:** SERVER-ONLY NEW INFORMATION (server spec Section 7.2; vectors
Section 4.6).
## Finding S-25: Reply sizing — negative lengths, the MTU cap, and the fatal ceiling
Three separate results that are easy to conflate.
**A negative Length is accepted and produces a minimum-size reply.** It is
returned in the open reply as sent. Echo replies for such a session are the size
of the field block the negotiated parameters imply — the ordinary rule of server
spec Section 9.2 applied to a negotiated length below that block. There is no
negative-size concept on the wire.
**On the two interfaces where the cap was reachable, the emitted reply is capped
at the bind interface's MTU.** Re-measured across three interfaces of one host:
| Bind interface (MTU) | negotiate 1500 | 1501 | 2000 | 8000 |
|----------------------|----------------|------|------|------|
| loopback (16384) | 1500 | 1501 | 2000 | 8000 |
| ethernet (1500) | 1500 | **1500** | **1500** | **1500** |
| tunnel (1280) | **1280** | **1280** | **1280** | **1280** |
On the 1500-byte and 1280-byte interfaces the cap is byte-exact — a negotiated
1281 produces 1280 — and the open reply still returns the requested value
unclamped, which is the Part II Finding S-14 result confirmed on one further
interface.
The loopback row is weaker evidence and is **not** an observation of a
16384-byte cap. It shows only that lengths up to 8000 were emitted unclamped. The
loopback cap cannot be approached on this host, because a reply beyond roughly
9300 bytes ends the server process before any such length is reached — see the
next result. **Host-specific:** these are this host's interface MTUs.
**A length the host cannot transmit terminates the process.** On the same host,
whose maximum outbound datagram size is 9216 bytes, replies up to 9216 bytes were
sent normally and negotiated lengths producing replies from roughly 9300 bytes
upward failed on the first echo and ended the server. With a wildcard bind, a
fatal send on the IPv4 listener took the healthy IPv6 listener down with it —
sessions on a different address family, unrelated to the offending one, stopped
being served at the same instant. **Host-specific:** 9216 is this host's outbound
datagram limit, not a protocol figure.
On the tested host, this failure was reachable in configurations whose effective
reply cap exceeded the host's outbound datagram ceiling, including the tested
loopback and wildcard cases. The tested 1500- and 1280-byte interface-bound
listeners could not reach it because their reply caps were lower. Jumbo interfaces
and other platforms were not tested.
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens Findings S-14 and
S-15 (server spec Sections 7.3, 9.2, 22.1, 23.5).
## Finding S-26: Over-MTU requests show an interface-MTU effective length boundary
Bound to an interface with a 1500-byte MTU, a 3000-byte request was dropped with
a configured maximum of 1499 and accepted with a maximum of 1500. On a 1280-byte
tunnel the same decision knee was at 1280. The observed maximum-length decision
therefore behaved as though its comparison length was capped at the tested
interface boundary. Separately, the policy was strict at the ordinary tested
sizes: with a maximum of 1000, a 1000-byte request was answered and a 1001-byte
request was dropped.
> **Black-box inference.** Receive-path truncation before the maximum-length
> decision is one explanation consistent with the 1499/1500 and 1279/1280
> boundaries, but the experiments cannot distinguish it from another mechanism
> that yields the same effective comparison length.
**Host-specific and environment-specific.** This is an observation on the
tested host and interfaces, not a wire rule. It does not prescribe a clean
server's receive strategy or maximum-length policy. Negotiated Length is not by
itself a valid receive-buffer upper bound, because mandatory protocol structure
can require a larger datagram.
**No capture accompanies this finding.** The capture taken during the experiment
was recorded on loopback with no maximum-length policy configured, and there all
three oversized requests were answered normally at the negotiated length; it
demonstrates neither the effective boundary nor a policy drop, and the behavior it does
show is already covered by server spec Section 5.5 and vectors Section 5.4. It
was reviewed and not admitted. The textual result above is the evidence.
**Classification:** SERVER-ONLY NEW INFORMATION, environment-specific (server spec
Section 9.4).
## Finding S-27: DSCP marking is per session, with unmarked opens and marked closes
Four sessions were opened on one listener negotiating 46, 8, 0 and 184, then
driven in three interleaved rounds. Every reply's IP header was read from the
kernel. Capture: `captures/server-dscp-interleaved.pcapng` (32 packets).
| Session | Requested | Open reply TOS | Echo reply TOS, rounds 1–3 |
|---------|-----------|----------------|-----------------------------|
| 1 | 46 | **0x00** | 0x2e, 0x2e, 0x2e |
| 2 | 8 | **0x00** | 0x08, 0x08, 0x08 |
| 3 | not requested | **0x00** | 0x00, 0x00, 0x00 |
| 4 | 184 | **0x00** | 0xb8, 0xb8, 0xb8 |
For each of these sessions, whose negotiated values are in `0..=255`, every echo
reply carried its own session's value verbatim; no reply carried another session's
value in any round, and every open reply was unmarked. Negative and out-of-range
values are addressed separately below.
**Server-initiated close replies: measured, but not shown by any admitted
capture.** The DSCP experiment recorded that a close-flagged echo reply carries
the same in-range session mark as an ordinary one. Neither admitted capture
demonstrates it, and a reader checking them will find nothing that does: the
interleaved capture above contains no close-flagged reply at all, and
`captures/server-close.pcapng` — which does contain one, at frame 36 — is TOS 0
throughout, because that session negotiated no DSCP. The result therefore rests
on the measurement, not on capture evidence, and the earlier wording that
explained it as "an ordinary marked echo reply that also sets the Close flag" has
been dropped: that phrasing argued from the flag rather than reporting what was
observed. A capture of a close-flagged reply on a session with a nonzero
negotiated DSCP would settle it directly and was not taken.
**On a listener that has already sent marked replies, an open reply is still
unmarked — evidenced separately.** This capture does not show that ordering: its
four open exchanges are frames 1–8 and its first marked echo reply is frame 10,
so no open reply in it follows a marked one. The ordering is instead established
by `captures/dscp-values.pcapng` from Part I, where one listener serves two
sessions in sequence: frame 4 is a marked echo reply (TOS `0x08`) and frame 7 is
the next session's open reply, unmarked, on the same port. The two pieces of
evidence are cited separately rather than merged.
Observed negative and out-of-range DSCP handling is host-specific, not protocol
behavior: a requested −1 appeared as TOS 255 on IPv4 and as traffic class 0 on
IPv6. A value of 256 or above was negotiated and echoed back, but no echo reply
was observed for that session while the server continued serving its other
sessions. The black-box result does not identify whether the absence arose while
applying the marking, during transmission, or at another stage.
**Classification:** SERVER-ONLY NEW INFORMATION; extends Findings B and C of
Part I (server spec Section 20; vectors Sections 10.1–10.3).
## Finding S-28: The default fill phase is continuous across the tested listeners
Finding S-16 recorded that the default fill pattern's phase is not reset per
packet or per session and carries over between sessions on one listener. It also
carries over between **listeners**: in the tested configuration — one server
bound to both an IPv4 and an IPv6 listener — successive echo replies drawn from a
session on each continued the same repeating pattern without resetting.
That is the scope of the result: phase continuity across the sessions tested and
across those two listeners. It is not established that a single stream is shared
by every session of a process in every configuration, and nothing here describes
how the bytes are produced.
**No recommendation is drawn from it.** The default fill is a fixed, public
pattern, so phase continuity across sessions discloses nothing about another
peer's traffic — unlike the no-fill mode of Finding S-16's second paragraph and
server spec Section 13.4, which is a real disclosure risk and carries its own
requirement there. A compatible server may reset the phase, continue it, or
derive the payload some other way. Those choices can produce different payload
bytes, making the phase policy observable in a packet capture, but they are
interoperability-equivalent because a conforming client cannot depend on payload
phase. No fill-state arrangement is prescribed.
**Classification:** SERVER-ONLY NEW INFORMATION; extends Finding S-16 (server spec
Section 13.3).
## Part IV Version Comparison
Every result in this part was checked against the 0.9.0 release and the tested
development build wherever a reproduction check was meaningful. All of the
following were **identical** on all three builds:
| Behavior | 0.9.0 | 0.9.1 | development build |
|----------|-------|-------|-------------------|
| Open whose restricted StampAt remains non-none and has no Clock is accepted, first echo fatal (S-19) | yes | yes | yes |
| Idle boundary at timeout + 5 s (S-22) | yes | yes | yes |
| Maximum-duration close at maximum + 2 s (S-23) | yes | yes | yes |
| A rate-limited request keeps the session alive (S-20) | yes | yes | yes |
| Interface-MTU effective length boundary (S-26) | yes | yes | yes |
| Burst allowance behavior | yes | yes | yes |
| Fill phase continuous across the tested listeners (S-28) | yes | yes | yes |
No new version difference was found. The single known wire-visible difference
between these builds remains the single-clock midpoint layout of Findings S-7 and
S-18, which this pass re-observed unchanged and which is not restated here.
## Part IV Captures Created
| File | Packets | Contents |
|------|---------|----------|
| `captures/server-clock-absent.pcapng` | 3 | An open selecting timestamps with no Clock tag is answered with a token and no Clock parameter; the first echo is the last packet in the exchange |
| `captures/server-expiry-consumption.pcapng` | 14 | Control versus foreign-endpoint-first, in one file: whether the packet that releases an expired session emits a final reply |
| `captures/server-dscp-interleaved.pcapng` | 32 | Four interleaved sessions with distinct DSCP values over three rounds; no cross-session leakage; unmarked open replies |
Each was container-rewritten to the convention already used for the captures of
Parts I–III — packet records and the rewriting tool's version string only, with
no capture comment, host, user, interface or filter metadata and no filesystem
path. Packet bytes and per-frame timestamps were verified identical before and
after, and the packet counts above are unchanged from the originals.
Two further captures from this pass were **not** admitted. One was materially
redundant with `captures/server-close.pcapng`, which already shows the
close-flagged reply, the trigger being counted, and the following packet
unanswered; a single-maximum capture cannot evidence the additive-margin result,
which is the new part. The other is described under Finding S-26. In both cases
the textual result is the evidence, which is the better trade.
## Part IV Client Spec Change Applied
| Section | Change | Class |
|---------|--------|-------|
| 19.3 | Maximum-duration deadline origin is the first echo request that is **served**; the close trigger is the first later echo that would otherwise be served. The two tested first-request drops do not start the clock, and a deadline-crossing rate-limited echo does not carry Close. The 2-second margin is additive across configured maxima | **CLARIFICATION** |
The client specification and the server specification are both compatibility
baselines, so the lifecycle rule is stated the same way in each. The earlier
client-side measurements are unaffected — the first request was served in those
runs — and no client behavior depends on the distinction, since a conforming
client stops sending at the negotiated duration and never reaches the deadline.
## Part IV Summary
- New upstream robustness defect documented: **S-19**, an accepted open that is
fatal on its first echo, reachable in the default configuration from two
unauthenticated datagrams, present in all three builds.
- Statements strengthened from thin or single-point evidence: the idle grace and
maximum-duration grace (**S-22**), the maximum-duration origin (**S-23**), the
rate-limit/idle interaction (**S-20**), the reply-length cap (**S-25**), and the
fill-phase scope (**S-28**).
- New parser-edge results: repeated tags and unknown-tag value encoding
(**S-24**), and the presence-versus-explicit-zero distinction that **S-19**
turns on.
- No result contradicted any statement in Parts I–III. **S-23** narrows the
origin recorded in Finding S-4 from "the first echo request" to "the first
echo request that is served"; the earlier measurement is unaffected, because
the first request was served in that experiment.
- Host-specific figures — interface MTUs, the 9216-byte outbound datagram limit,
the effective-length decision knees, and the socket handling of out-of-range DSCP — are
labelled as such wherever they appear and are not protocol constants.
- Nothing in this part rests on anything but observed request bytes, reply bytes,
reply presence, IP header markings, timings, and whether the server was still
serving afterwards.
+34 -8
View File
@@ -773,11 +773,20 @@ The following configuration parameters affect protocol behavior:
produce the wire byte) or the raw 0–255 byte is a presentation decision for 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 that project, to be made and implemented separately. It is not a protocol
fact and this specification does not prescribe it. fact and this specification does not prescribe it.
- **Protocol:** The server echoes packets with the negotiated DSCP value. - **Protocol:** For negotiated values in `0..=255`, the server echoes packets
The server MAY disallow DSCP and reset it to 0. with that DSCP value. The server MAY disallow DSCP and reset it to 0.
Negative and out-of-range handling is host-specific; see
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 20.
- **Observed behavior:** DSCP/TOS is applied only to echo request and echo - **Observed behavior:** DSCP/TOS is applied only to echo request and echo
reply packets. Open and close packets use TOS=0 regardless of the reply packets. Open and close packets use TOS=0 regardless of the
negotiated DSCP value. [**Verified 2026-04-28** — Finding C.] negotiated DSCP value. [**Verified 2026-04-28** — Finding C.]
- **Scope of the previous bullet (clarified 2026-08-12).** "Open and close
packets" means **standalone** datagrams: the open request, the open reply, and
the client's close request. A **server-initiated close** is not one of them —
it is an ordinary echo reply that additionally sets the Close flag, and it
carries the session's in-range negotiated marking like any other echo reply. A
client must not expect such a reply to arrive unmarked. See
`IRTT_SERVER_PROTOCOL_SPEC.md` Sections 15.2 and 20.
### 10.9 Open Timeouts ### 10.9 Open Timeouts
@@ -1518,12 +1527,29 @@ maximum default is 76 bytes (both+both+both+HMAC).
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64. **Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
**Result:** Yes. A server that enforces a maximum test duration answers the **Result:** Yes. A server that enforces a maximum test duration answers the
first echo request arriving after its hard deadline (the configured maximum first otherwise-serviceable echo request after its hard deadline with a complete,
duration plus a 2-second grace, measured from the session's first echo request) ordinary echo reply that
with a complete, ordinary echo reply that additionally has the Close flag set, additionally has the Close flag set, and removes the session at the same moment.
and removes the session at the same moment. Measured: a 1-second maximum The deadline is the configured maximum duration plus a 2-second grace, measured
produced a close-flagged reply 3.01 s after the first echo; a 2-second maximum from the session's first echo request that is actually **served**. Measured: a
produced one at 4.06 s. 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.
*Refined 2026-08-12.* This entry previously said the deadline was measured from
the session's first echo request, and that the first request after the deadline
carries the flag. A later boundary sweep narrowed both ends. For the **origin**,
a request that is dropped — the two classes tested were exceeding the maximum
length and arriving from a foreign source endpoint — does not start the clock,
and a later served echo does. For the **trigger**, a deadline-crossing request
that was rate-limited did not carry the flag; the next served echo did. The
corresponding behavior for other independently dropped post-deadline request
classes was not tested and is not specified here. The earlier measurements above
are unaffected, because their first request was served and their triggering
request was too. The same sweep showed the 2-second margin to be additive
across configured maxima of 500 ms, 1 s and 3 s. See
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2 and
`BLACKBOX_VERIFICATION_REPORT.md` Finding S-23; nothing a client does depends on
the distinction, since a conforming client never reaches the deadline.
A conforming client never reaches this deadline, because it stops sending at the A conforming client never reaches this deadline, because it stops sending at the
negotiated duration. The mechanism exists to bound sessions from clients that negotiated duration. The mechanism exists to bound sessions from clients that
+620 -70
View File
@@ -1,7 +1,7 @@
# IRTT-Compatible Server Protocol Specification # IRTT-Compatible Server Protocol Specification
**Version:** 1.1-verified **Version:** 1.2-verified
**Date:** 2026-08-11 **Date:** 2026-08-12
**Compatibility baseline:** IRTT 0.9.1, protocol version 1 **Compatibility baseline:** IRTT 0.9.1, protocol version 1
**Verification platform:** macOS Darwin 25.5.0 arm64 **Verification platform:** macOS Darwin 25.5.0 arm64
@@ -28,17 +28,23 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT",
"RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as interoperability "RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as interoperability
chatgpt-codex-connector[bot] commented 2026-08-12 08:00:17 +00:00 (Migrated from github.com)
Review

P2 Badge Make the inference definition match its uses

This definition says black-box inferences never describe an internal mechanism, but the document immediately uses that marker for a token-bucket mechanism in Section 9.3 and for lookup/creation-driven expiry rather than scheduled removal in Section 18.2. Those are appropriately qualified hypotheses, but they still describe mechanisms, so the statement-level definition contradicts its own uses; define the level as not asserting a mechanism as fact, or remove the mechanism descriptions.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Make the inference definition match its uses** This definition says black-box inferences never describe an internal mechanism, but the document immediately uses that marker for a token-bucket mechanism in Section 9.3 and for lookup/creation-driven expiry rather than scheduled removal in Section 18.2. Those are appropriately qualified hypotheses, but they still describe mechanisms, so the statement-level definition contradicts its own uses; define the level as not *asserting* a mechanism as fact, or remove the mechanism descriptions. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/47bf6a2f0f563ca91365a13c4b97a67f01246fb9/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:17:18 +00:00 (Migrated from github.com)
Review

Addressed in 064b7d550107c7e4d10cd96c93796c8d2731e07b. The definition now permits explicitly labelled explanatory mechanism hypotheses, but does not treat those mechanisms as directly observed fact and keeps them non-load-bearing. Section 18.2 now also describes its expiry explanation as not directly measured.

Addressed in 064b7d550107c7e4d10cd96c93796c8d2731e07b. The definition now permits explicitly labelled explanatory mechanism hypotheses, but does not treat those mechanisms as directly observed fact and keeps them non-load-bearing. Section 18.2 now also describes its expiry explanation as not directly measured.
requirements, consistent with RFC 2119. requirements, consistent with RFC 2119.
Five distinct levels of statement are used throughout, and they are not Six distinct levels of statement are used throughout, and they are not
interchangeable. Nothing in one level implies anything at another. interchangeable. Nothing in one level implies anything at another.
| Marker | Meaning | | Marker | Meaning |
|--------|---------| |--------|---------|
| **Protocol requirement** | Required for interoperability with existing clients. Stated with MUST / MUST NOT / SHOULD. Every one rests on observed behavior. | | **Protocol requirement** | Required for interoperability with existing clients. Stated with MUST / MUST NOT / SHOULD. Every one rests on observed behavior. |
| **Upstream 0.9.1 behavior** | What the reference server was observed to do on the wire. A compatible implementation is **not** obliged to reproduce it unless a separate protocol requirement says so. | | **Upstream 0.9.1 behavior** | What the reference server was observed to do on the wire. A compatible implementation is **not** obliged to reproduce it unless a separate protocol requirement says so. Includes the observed robustness defects of Section 22, which a compatible implementation specifically should **not** reproduce. |
| **Black-box inference** | A conclusion or explanatory hypothesis drawn from several observations rather than directly observed on the wire. Always labelled as an inference and accompanied by its supporting observations. It may describe a mechanism consistent with the evidence, but does not assert that mechanism as measured fact. Never load-bearing: no requirement depends on one. |
| **Server policy** | A configurable or arbitrary choice of the reference server, including its defaults. A compatible implementation may choose differently, and in several cases should. | | **Server policy** | A configurable or arbitrary choice of the reference server, including its defaults. A compatible implementation may choose differently, and in several cases should. |
| **Robustness recommendation** | Advice for the clean implementation's own resilience or safety. It is **not** an interoperability requirement and no existing client can observe whether it was followed. | | **Robustness recommendation** | Advice for the clean implementation's own resilience or safety. It is **not** an interoperability requirement. Its consequences may nevertheless be externally observable, particularly when malformed or adversarial traffic affects unrelated conforming sessions. |
| **Source-assisted historical conclusion** | A judgement about upstream's release history reached with contaminated-side access to upstream material. Never load-bearing: no requirement in this document depends on one. Used once, in Section 11.3.1. | | **Source-assisted historical conclusion** | A judgement about upstream's release history reached with contaminated-side access to upstream material. Never load-bearing: no requirement in this document depends on one. Used once, in Section 11.3.1. |
Several statements are additionally marked **host-specific** or
**environment-specific**. Those record what one tested host and its interfaces
did. They are never protocol constants, and the figures in them must not be
compiled into an implementation.
Where the reference server's own behavior would be a poor choice for a new Where the reference server's own behavior would be a poor choice for a new
implementation — an unbounded resource policy, a fatal reaction to a single implementation — an unbounded resource policy, a fatal reaction to a single
failed send — this document records the observation and the recommendation failed send — this document records the observation and the recommendation
@@ -181,8 +187,7 @@ produces these accept/discard decisions is conformant, whatever order it
evaluates them in. evaluates them in.
**Discard conditions.** Each of the following was independently observed to **Discard conditions.** Each of the following was independently observed to
cause the datagram to be **silently discarded**, with no reply of any kind and cause the datagram to be **silently discarded**, with no reply of any kind:
no change to the state of any live session:
| Condition | Notes | | Condition | Notes |
|-----------|-------| |-----------|-------|
@@ -193,12 +198,20 @@ no change to the state of any live session:
| Authentication mismatch of any kind | Section 14; includes the flag/key mismatch in either direction | | Authentication mismatch of any kind | Section 14; includes the flag/key mismatch in either direction |
| A non-open datagram too short to carry the token | | | A non-open datagram too short to carry the token | |
| A non-open datagram whose token matches no live session | Section 8 | | A non-open datagram whose token matches no live session | Section 8 |
| A non-open datagram whose token matches a session bound to a different source endpoint | Section 8; the session itself is unaffected | | A non-open datagram whose token matches a session bound to a different source endpoint | Section 8; see also the expired-session case in Section 18.2 |
| An echo datagram too short to carry the sequence number | | | An echo datagram too short to carry the sequence number | |
| An echo datagram exceeding a configured maximum packet length | Section 9.4, server policy | | An echo datagram exceeding a configured maximum packet length | Section 9.4, server policy |
| An echo datagram arriving with no rate allowance left | Section 9.3, server policy | | An echo datagram arriving with no rate allowance left | Section 9.3, server policy |
| An open datagram whose parameter payload is malformed or out of range | Section 7.2 | | An open datagram whose parameter payload is malformed or out of range | Section 7.2 |
**Tested externally visible effects.** Every tested discarded echo left the
reported reception count and received window unchanged. A rate-limited request
is the known exception to any broader claim that a discard has no externally
observable session effect: it extends the observed idle lifetime. Section 9.3
contains the detailed comparison. No general claim about hidden state, or about
the effects of every discard class at an expiry boundary, is made here; Section
18.2 records the tested expired-session release cases.
**Protocol requirement.** Every rejection is a **silent discard**. The protocol **Protocol requirement.** Every rejection is a **silent discard**. The protocol
defines no error reply, no reset, and no NACK. A client distinguishes "rejected" defines no error reply, no reset, and no NACK. A client distinguishes "rejected"
from "lost" only by timing out. A compatible server MUST NOT answer any of the from "lost" only by timing out. A compatible server MUST NOT answer any of the
2
@@ -331,9 +344,12 @@ A server parses the whole parameter payload. Observed acceptance rules:
| Duration present and ≤ 0 | **Dropped, no reply** | | Duration present and ≤ 0 | **Dropped, no reply** |
chatgpt-codex-connector[bot] commented 2026-08-12 07:37:18 +00:00 (Migrated from github.com)
Review

P2 Badge Exempt no-timestamp servers from the fatal Clock row

When the server is configured to provide no timestamps, Section 22.5 and the behavioral vector say it restricts the requested non-none StampAt to none and serves this absent-Clock session normally. This unconditional admission-table row instead says every such request creates an unusable session whose first echo is fatal; qualify it for the default/single-timestamp policies or include the no-timestamp exception so the compatibility baseline does not prescribe opposite outcomes.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Exempt no-timestamp servers from the fatal Clock row** When the server is configured to provide no timestamps, Section 22.5 and the behavioral vector say it restricts the requested non-none `StampAt` to none and serves this absent-Clock session normally. This unconditional admission-table row instead says every such request creates an unusable session whose first echo is fatal; qualify it for the default/single-timestamp policies or include the no-timestamp exception so the compatibility baseline does not prescribe opposite outcomes. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 07:54:34 +00:00 (Migrated from github.com)
Review

Addressed in 47bf6a2f0f563ca91365a13c4b97a67f01246fb9. The fatal absent-Clock result is now scoped to cases where restriction leaves the restricted (effective) StampAt non-none; the tested no-timestamp policy restricts StampAt to none and serves the session normally.

Addressed in 47bf6a2f0f563ca91365a13c4b97a67f01246fb9. The fatal absent-Clock result is now scoped to cases where restriction leaves the restricted (effective) StampAt non-none; the tested no-timestamp policy restricts StampAt to none and serves the session normally.
| Interval present and ≤ 0 | **Dropped, no reply** | | Interval present and ≤ 0 | **Dropped, no reply** |
| Duration or Interval absent (implicitly 0) | Accepted | | Duration or Interval absent (implicitly 0) | Accepted |
| ReceivedStats outside 0–3 | **Dropped, no reply** | | ReceivedStats outside 0–3, or negative | **Dropped, no reply** |
| StampAt outside 0–4 | **Dropped, no reply** | | StampAt outside 0–4, or negative | **Dropped, no reply** |
| Clock outside 1–3 | **Dropped, no reply** | | Clock outside 1–3, **including an explicit 0** | **Dropped, no reply** |
| Clock **absent** with requested StampAt = none | Accepted; no timestamps are emitted |
| Clock **absent** with requested StampAt ≠ none, where restriction leaves StampAt non-none | The **open** is answered normally, with a valid token. The session it creates is not usable: the first echo request on it is fatal to the tested upstream server. See Section 22.5 |
| Clock **absent** with requested StampAt ≠ none, where restriction changes StampAt to none | Accepted; the session serves normally without timestamps. Observed with the no-timestamp server policy (Sections 11.4 and 22.5). |
| Length any value, including negative | Accepted (see Section 7.3) | | Length any value, including negative | Accepted (see Section 7.3) |
| DSCP any value, including negative or > 255 | Accepted (see Section 7.3) | | DSCP any value, including negative or > 255 | Accepted (see Section 7.3) |
| ServerFill string longer than 32 bytes | **Dropped, no reply** | | ServerFill string longer than 32 bytes | **Dropped, no reply** |
6
@@ -350,6 +366,74 @@ parameter payloads. A payload of over 200 bytes composed of unknown tags was
accepted and answered normally. Any such limit in an implementation is a local accepted and answered normally. Any such limit in an implementation is a local
chatgpt-codex-connector[bot] commented 2026-08-12 08:24:41 +00:00 (Migrated from github.com)
Review

P2 Badge Scope invalid duplicate handling to the orders actually tested

The vectors at SERVER_BEHAVIORAL_VECTORS.md lines 427–435 exercise an invalid occurrence in both positions only for Duration; Clock and ServerFill are tested only with the invalid value second, and the other known tags have no invalid-duplicate rows. Therefore the parser-wide claim that any invalid occurrence rejects regardless of position exceeds the admitted evidence and could misdirect an implementation whose validation differs by parameter type. Scope the position-independent rule to Duration and the explicitly tested rows, or label the broader conclusion as an inference/remaining ambiguity.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope invalid duplicate handling to the orders actually tested** The vectors at `SERVER_BEHAVIORAL_VECTORS.md` lines 427–435 exercise an invalid occurrence in both positions only for Duration; Clock and ServerFill are tested only with the invalid value second, and the other known tags have no invalid-duplicate rows. Therefore the parser-wide claim that any invalid occurrence rejects regardless of position exceeds the admitted evidence and could misdirect an implementation whose validation differs by parameter type. Scope the position-independent rule to Duration and the explicitly tested rows, or label the broader conclusion as an inference/remaining ambiguity. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/064b7d550107c7e4d10cd96c93796c8d2731e07b/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:46:59 +00:00 (Migrated from github.com)
Review

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. Valid last-value-wins evidence remains broad across the nine tested known tags. Invalid-duplicate behavior is now scoped to Duration in both tested orders and the tested invalid-second Clock and ServerFill rows; untested orders and parameters are explicitly unspecified in the spec, vectors, verification report, and PR body.

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. Valid last-value-wins evidence remains broad across the nine tested known tags. Invalid-duplicate behavior is now scoped to Duration in both tested orders and the tested invalid-second Clock and ServerFill rows; untested orders and parameters are explicitly unspecified in the spec, vectors, verification report, and PR body.
buffer-sizing choice, not a protocol rule. buffer-sizing choice, not a protocol rule.
**Wire fact and upstream 0.9.1 behavior.** The two Clock rows above are the
sharpest instance of a general point: an *omitted* tag and an explicitly encoded
`0` are distinct encodings, and upstream produces distinct outcomes for them.
An explicit `Clock = 0` is out of range and the open is dropped; an omitted Clock
is accepted.
**Compatibility consequence.** An implementation that chooses to reproduce
those distinct upstream outcomes needs enough information during request
validation to distinguish the two forms.
**Clean policy.** A clean server MAY intentionally map otherwise distinct
adversarial forms to the same compatible, safe outcome when no conforming client
depends on the distinction. This does not prescribe any decoded representation
or require retaining presence after validation.
The two directions differ, and the distinction must not be over-applied. In a
**request**, a present zero is an ordinary encoding a client may send: `Length =
0` and `DSCP = 0` are both accepted, and `Clock = 0` is rejected precisely
*because* it is present carrying that value. In an **open reply**, a parameter
whose restricted value is zero is omitted entirely (Section 7.4), so a server
never emits a present-as-zero form — which is why a client reconstructs an absent
parameter as zero when reading a reply. An implementation reproducing the
upstream request outcomes must not treat an omitted request parameter as an
explicit zero during validation.
**Upstream 0.9.1 behavior — repeated occurrences of a known tag.** A parameter
tag may appear more than once in one payload. Among occurrences that are each
individually valid, the **last** one takes effect; this was measured for
ProtocolVersion, Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP
and ServerFill.
**Invalid repeated values were tested more narrowly.** For Duration, an invalid
zero occurrence caused the open to be dropped in both tested positions: valid
then invalid, and invalid then valid. For Clock and ServerFill, an invalid second
occurrence also caused the open to be dropped. The opposite ordering was not
tested for Clock or ServerFill, and invalid duplicate occurrences were not tested
for ProtocolVersion, Interval, Length, ReceivedStats, StampAt or DSCP. The
evidence therefore does not establish a parser-wide, position-independent
invalid-duplicate rule for every parameter. See the vectors file, Section 4.6.
**Protocol requirement.** None: no conforming client emits a repeated tag. A
compatible server MAY resolve repeats however it likes. The behavior is recorded
because a decoder that stops at the first occurrence, or that validates only the
occurrence it keeps, can diverge from the measured upstream outcomes on these
adversarial inputs.
**Upstream 0.9.1 behavior — unknown tags.** The value of an unknown tag is
consumed as a varint and discarded. Tag numbers 0, 10, 11, 100, 127, 128, 1000
and 2³² were each tested and each ignored, with the remainder of the payload
still parsed and the open answered normally; an open carrying *only* unknown tags
creates an ordinary default session. No length-prefixed or string-shaped value
form was accepted for an unknown tag.
> **Black-box inference, not an observation.** Because the only value encoding
> accepted for an unknown tag is a varint, an unknown tag cannot carry a byte
> string, and the wire format therefore offers no general extension channel for
> structured values. That is a conclusion drawn from the encodings tested above,
> not a documented property, and this specification does not define an extension
> mechanism.
**Upstream 0.9.1 behavior — a rejected open does not poison the next open.**
After each tested malformed payload was dropped — incomplete trailing varint, a
tag with no value, a length-prefixed string running past the end of the buffer,
or a varint overflow — a well-formed open sent immediately afterwards from the
same endpoint was accepted and served normally. The experiment establishes no
persistent externally visible parser or session effect on the following request;
it does not establish whether any hidden accounting or temporary state exists.
### 7.3 Server Restriction of Parameters ### 7.3 Server Restriction of Parameters
A server MAY reduce or replace requested parameters. The restricted values are A server MAY reduce or replace requested parameters. The restricted values are
@@ -386,7 +470,23 @@ client aborts; idle timeout 4 s produces no reduction at all for a 1 s request.
enforced minimum remains the larger. A fully conforming client then sends at the enforced minimum remains the larger. A fully conforming client then sends at the
negotiated interval and is rate-limited (Section 9.3). Observed with a 5 s negotiated interval and is rate-limited (Section 9.3). Observed with a 5 s
minimum interval and an 8 s idle timeout: negotiated interval 2 s, but only the minimum interval and an 8 s idle timeout: negotiated interval 2 s, but only the
first request in each 5 s window was answered. first request in each 5 s window was answered. The floor is applied before the
cap, so the returned interval can also fall *below* a configured minimum:
measured with a 5 s minimum interval and a 4 s idle timeout, the returned
interval was 1 s. Further boundary rows are in the vectors file, Section 4.4.
**Upstream 0.9.1 behavior — a negative Length.** A negative Length is accepted
during negotiation and returned in the open reply as the value the client sent.
There is no negative-size concept on the wire: echo replies for such a session
are the size of the field block the negotiated parameters imply, which is what
the ordinary reply-length rule of Section 9.2 produces for any negotiated length
at or below that block. Clamping happens at the reply, not at the negotiation.
**No requirement follows from this.** That upstream accepts a negative Length
does not make acceptance an interoperability requirement, and this specification
does not decide whether a clean server should accept, clamp or reject one. No
conforming client emits a negative Length, and a client that did would compare
the returned value against what it requested in the ordinary way.
### 7.4 Open Reply ### 7.4 Open Reply
@@ -491,12 +591,12 @@ Observed outcomes:
| Case | Result | | Case | Result |
|------|--------| |------|--------|
| Correct token, original endpoint | Served normally | | Correct token, original endpoint | Served normally |
| Correct token, **different source port**, same IP | **Dropped, no reply.** Session left intact and still usable from the original port | | Correct token, **different source port**, same IP | **Dropped, no reply.** For a live, non-expired session, it remains usable from the original port; see Section 18.2 for the separately observed expired-session release case. |
| Correct token, different address family | **Dropped, no reply** (different listener, unknown token there) | | Correct token, different address family | **Dropped, no reply** (different listener, unknown token there) |
| Unknown token | **Dropped, no reply** | | Unknown token | **Dropped, no reply** |
| Zero token | **Dropped, no reply** (treated as any other unknown token) | | Zero token | **Dropped, no reply** (treated as any other unknown token) |
| Token of a session already closed | **Dropped, no reply** | | Token of a session already closed | **Dropped, no reply** |
| **Close** request bearing a valid token from a foreign source port | **Dropped**; the session is **not** closed | | **Close** request bearing a valid token from a foreign source port | **Dropped**; it does not close an otherwise live, non-expired session. Section 18.2 covers the separately observed expired-session release case. |
The last row matters: endpoint binding protects the close path as well as the The last row matters: endpoint binding protects the close path as well as the
echo path. A capture of these cases is in echo path. A capture of these cases is in
2
@@ -606,10 +706,39 @@ produced 1500-byte replies, while the open reply still reported the requested
value. Upstream clients treat a reply shorter than the negotiated length as a value. Upstream clients treat a reply shorter than the negotiated length as a
fatal error, so this configuration fails on the first echo. See Section 22. fatal error, so this configuration fails on the first echo. See Section 22.
Re-measured across three interfaces of one host:
| Bind interface (MTU) | 1500 | 1501 | 2000 | 8000 |
|----------------------|------|------|------|------|
| loopback (16384) | 1500 | 1501 | 2000 | 8000 |
| ethernet (1500) | 1500 | **1500** | **1500** | **1500** |
| tunnel (1280) | **1280** | **1280** | **1280** | **1280** |
On the **1500-byte and 1280-byte** interfaces the cap was that interface's MTU
exactly, byte for byte — a negotiated 1281 produced 1280 — and a negotiated
length at or below the MTU was emitted in full.
The loopback row establishes something weaker, and is not evidence of an exact
16384-byte cap. All it shows is that lengths up to 8000 were emitted unclamped.
The cap could not be approached there: on this host a reply beyond roughly 9300
chatgpt-codex-connector[bot] commented 2026-08-12 05:32:54 +00:00 (Migrated from github.com)
Review

P2 Badge Classify rate-limit activity for capped intervals

When the configured minimum interval exceeds idle timeout / 4, Section 7.3 explicitly says a fully conforming client sends at the shorter negotiated interval and is rate-limited, so the rationale here that conforming clients never encounter this state is false. For example, if the token refill interval also exceeds the idle deadline, refreshing on each dropped probe preserves the session until a later probe can be served, while ignoring those probes expires it before refill; that difference is observable by the conforming client. The behavior therefore cannot remain an unrestricted MAY under the configuration model documented by this specification.

AGENTS.md reference: AGENTS.md:L20-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Classify rate-limit activity for capped intervals** When the configured minimum interval exceeds `idle timeout / 4`, Section 7.3 explicitly says a fully conforming client sends at the shorter negotiated interval and is rate-limited, so the rationale here that conforming clients never encounter this state is false. For example, if the token refill interval also exceeds the idle deadline, refreshing on each dropped probe preserves the session until a later probe can be served, while ignoring those probes expires it before refill; that difference is observable by the conforming client. The behavior therefore cannot remain an unrestricted `MAY` under the configuration model documented by this specification. AGENTS.md reference: [AGENTS.md:L20-L23](https://github.com/Lochnair/irtt-rs/blob/0ca8988671c0f6a470de79a86c55c87a0fc3b161/AGENTS.md#L20-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 06:18:30 +00:00 (Migrated from github.com)
Review

Addressed in 2ed0e257. The false claim that conforming clients cannot be rate-limited was removed: the documented restriction interaction means a conforming client can encounter rate limiting. The combined liveness consequence is explicitly a black-box inference from separately measured behaviors and was deliberately not promoted to an unsupported unconditional RFC 2119 requirement.

Addressed in 2ed0e257. The false claim that conforming clients cannot be rate-limited was removed: the documented restriction interaction means a conforming client can encounter rate limiting. The combined liveness consequence is explicitly a black-box inference from separately measured behaviors and was deliberately not promoted to an unsupported unconditional RFC 2119 requirement.
chatgpt-codex-connector[bot] commented 2026-08-12 06:28:09 +00:00 (Migrated from github.com)
Review

P2 Badge Name the configured interval as the limiter input

When the configured minimum exceeds idle timeout / 4, these lines establish that the returned interval can be 2 seconds while the limiter still enforces 5 seconds, but Section 9.3 says the server “enforces the negotiated minimum send interval.” An implementer following that sentence would refill at the returned 2-second cadence rather than the observed 5-second cadence; rewrite the limiter description to distinguish the configured enforcement interval from the negotiated value.

AGENTS.md reference: AGENTS.md:L18-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Name the configured interval as the limiter input** When the configured minimum exceeds `idle timeout / 4`, these lines establish that the returned interval can be 2 seconds while the limiter still enforces 5 seconds, but Section 9.3 says the server “enforces the negotiated minimum send interval.” An implementer following that sentence would refill at the returned 2-second cadence rather than the observed 5-second cadence; rewrite the limiter description to distinguish the configured enforcement interval from the negotiated value. AGENTS.md reference: [AGENTS.md:L18-L23](https://github.com/Lochnair/irtt-rs/blob/2ed0e2570ea18b118e78d9f4f196d0051e4963e7/AGENTS.md#L18-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 06:50:33 +00:00 (Migrated from github.com)
Review

Addressed in 389c26b. Section 9.3 now distinguishes the configured minimum interval used by the tested limiter from the negotiated interval returned to the client; token-bucket wording is explicitly a black-box inference.

Addressed in 389c26b. Section 9.3 now distinguishes the configured minimum interval used by the tested limiter from the negotiated interval returned to the client; token-bucket wording is explicitly a black-box inference.
bytes ends the server process (Section 22.1), well below the loopback MTU, so no
observation of that boundary is possible on this configuration.
This is a property of the tested host and its interfaces, not a wire constant.
Whether the cap is always the listener interface's MTU on other platforms remains
open; see Section 23.5. Note also that the tested loopback and wildcard
configurations allowed a negotiated length to exceed what this host will actually
transmit — the hazard of Section 22.1.
### 9.3 Rate Limiting ### 9.3 Rate Limiting
**Server policy.** The reference server enforces the negotiated minimum send **Server policy.** The tested upstream rate limiter replenishes reply allowance
interval with a token-bucket allowance: using the configured minimum send interval. This may differ from the interval
returned during negotiation when the idle-timeout restriction of Section 7.3
reduces the negotiated value below that configured minimum.
**Black-box inference.** The observed burst and replenishment behavior is
consistent with a token-bucket allowance:
- a burst allowance of *N* requests is available immediately (default N = 5); - a burst allowance of *N* requests is available immediately (default N = 5);
- allowance is replenished at one request per configured minimum interval and - allowance is replenished at one request per configured minimum interval and
@@ -623,7 +752,61 @@ were all answered. With a burst of 1, a blast of 4 produced 1 reply.
**Protocol requirement.** A rate-limited request MUST NOT be answered and MUST **Protocol requirement.** A rate-limited request MUST NOT be answered and MUST
NOT affect the reception statistics (Section 10). Measured: the received count NOT affect the reception statistics (Section 10). Measured: the received count
advanced 1, 2, 3, 4, 5 across the accepted burst and continued 6, 7, 8 for the advanced 1, 2, 3, 4, 5 across the accepted burst and continued 6, 7, 8 for the
later requests — dropped requests were not counted. later requests — dropped requests were not counted. Neither the count nor the
received window advances for a rate-limited request.
**Upstream 0.9.1 behavior — a rate-limited request still counts as activity for
idle expiry.** This was the only tested drop class that did. A rate-limited request
receives no reply and advances neither statistic, yet it extends the session's
observed idle lifetime exactly as a served request would. Every other drop class
tested leaves the idle deadline where it was:
| Injected packet | Counted / window advanced | Extends the observed idle lifetime |
|-----------------|---------------------------|------------------------------------|
| Bad magic | no | no |
| Invalid flag bits | no | no |
| Reply or Open flag on a data packet | no | no |
| Unknown token, or a zero token | no | no |
| Valid token from a foreign source endpoint | no | no — but see Section 18.2 |
| Echo truncated before the sequence number | no | no |
| Request over the configured maximum length | no | no |
| Bad, missing or zeroed authentication | no | no |
| **Request with no rate allowance left** | **no** | **YES** |
The rate-limit row was established three ways against each other, and reproduced
on the 0.9.0 release, the 0.9.1 release and the tested post-0.9.1 development
chatgpt-codex-connector[bot] commented 2026-08-12 05:32:54 +00:00 (Migrated from github.com)
Review

P2 Badge Remove the false request-length invariant

When the negotiated length is smaller than the mandatory echo header, a conforming client does send a request larger than the negotiated length: this same section documents a session negotiating length 10 whose requests are necessarily 16 bytes at lines 775–778. Retaining this assertion can lead an implementer to size or validate request handling from the negotiated value and miss the minimum-header case; explain the over-MTU result without claiming that request length is always bounded by the negotiated length.

AGENTS.md reference: AGENTS.md:L20-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Remove the false request-length invariant** When the negotiated length is smaller than the mandatory echo header, a conforming client does send a request larger than the negotiated length: this same section documents a session negotiating length 10 whose requests are necessarily 16 bytes at lines 775–778. Retaining this assertion can lead an implementer to size or validate request handling from the negotiated value and miss the minimum-header case; explain the over-MTU result without claiming that request length is always bounded by the negotiated length. AGENTS.md reference: [AGENTS.md:L20-L23](https://github.com/Lochnair/irtt-rs/blob/0ca8988671c0f6a470de79a86c55c87a0fc3b161/AGENTS.md#L20-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 06:18:33 +00:00 (Migrated from github.com)
Review

Addressed in 2ed0e257. Removed the false request-length invariant from the over-MTU observation and now state only the measured-safe consequence: Negotiated Length is not by itself a valid receive-buffer upper bound because mandatory protocol structure can require a larger datagram.

Addressed in 2ed0e257. Removed the false request-length invariant from the over-MTU observation and now state only the measured-safe consequence: Negotiated Length is not by itself a valid receive-buffer upper bound because mandatory protocol structure can require a larger datagram.
build: a session fed *only* too-fast requests outlived its idle deadline; the
same cadence of oversized requests did not; and a silent session expired on time.
This is a statement about the session's externally observable lifetime, and about
nothing else. What a server updates internally when it declines a request, and
whether a lifetime is tracked as a timestamp, a deadline or anything else, is not
observable and is not described here.
**Compatibility significance — black-box inference.** The earlier rationale
that a conforming client can never observe this behavior is false. Section 7.3
records a configuration where the interval returned to the client is shorter than
the configured minimum interval used by the tested limiter: when the configured
minimum exceeds (idle timeout ÷ 4), the negotiated interval is the smaller value
while reply allowance still replenishes on the larger configured interval. A
client that sends at the interval it was given can therefore itself be
chatgpt-codex-connector[bot] commented 2026-08-12 06:28:09 +00:00 (Migrated from github.com)
Review

P2 Badge Label the pre-policy truncation as an inference

The 1499/1500 acceptance knees establish only the effective length used by the maximum-length decision; they cannot show that the datagram reached that policy already truncated or that its true length was never compared, because an implementation that receives all 3000 bytes and caps the comparison at the interface MTU has the same wire behavior. State that processing-order conclusion as an inference or leave the truncation point ambiguous rather than presenting an unobservable internal path as a measured fact.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Label the pre-policy truncation as an inference** The 1499/1500 acceptance knees establish only the effective length used by the maximum-length decision; they cannot show that the datagram reached that policy already truncated or that its true length was never compared, because an implementation that receives all 3000 bytes and caps the comparison at the interface MTU has the same wire behavior. State that processing-order conclusion as an inference or leave the truncation point ambiguous rather than presenting an unobservable internal path as a measured fact. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/2ed0e2570ea18b118e78d9f4f196d0051e4963e7/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 06:50:21 +00:00 (Migrated from github.com)
Review

Addressed in 389c26b. The measured 1499/1500 and 1279/1280 decision boundaries were retained; the claim that truncation occurred before policy was removed or labelled only as an inference because black-box evidence cannot establish that internal ordering.

Addressed in 389c26b. The measured 1499/1500 and 1279/1280 decision boundaries were retained; the claim that truncation occurred before policy was removed or labelled only as an inference because black-box evidence cannot establish that internal ordering.
rate-limited.
Consequently, counting rate-limited requests as idle activity is not
categorically unobservable. Combining that documented restriction interaction
with the directly measured idle-refresh behavior above suggests configurations
where liveness depends on whether rate-limited requests count as activity. That
combined liveness consequence was not independently measured as one experiment,
so this specification does not assign it an unconditional RFC 2119
interoperability requirement. It records the interaction for later
implementation-policy work; a clean server may, for example, avoid negotiating a
cadence inconsistent with the limiter it actually enforces.
Separately, the same behavior matters in the opposite direction: a peer that
floods a session fast enough to be rate-limited can keep it serving past its
otherwise observed idle deadline without receiving a reply. That is a
consideration for the bounded resource policy of Section 18.3 rather than a
compatibility rule.
### 9.4 Maximum Request Length ### 9.4 Maximum Request Length
@@ -640,6 +823,29 @@ request is dropped while opens still succeed. Measured with a maximum of 10: the
session opened, negotiated a length of 10, and every 16-byte echo request was session opened, negotiated a length of 10, and every 16-byte echo request was
dropped. dropped.
The comparison is strict and is made against the length the server **received**.
Measured with a maximum of 1000: a 1000-byte request was answered and a 1001-byte
request was dropped.
**Environment-specific observation — an over-MTU request shows an
interface-MTU effective length boundary.** On the tested host, a 3000-byte
request sent to a 1500-byte-MTU interface was dropped with a configured maximum
of 1499 and accepted with a maximum of 1500. On a 1280-byte tunnel interface the
same decision knee appeared at 1280. In these experiments, the maximum-length
decision therefore behaved as though its comparison length was capped at the
tested interface boundary.
> **Black-box inference.** Receive-path truncation before the maximum-length
> decision is one explanation consistent with this observation, but the
> experiment cannot distinguish it from receiving the full datagram and
> normalizing or capping the comparison length, or from another mechanism that
> yields the same effective boundary.
This is an observation on the tested host and interfaces, not a wire rule. It
does not constrain a clean implementation's receive strategy or maximum-length
policy. Negotiated Length is not by itself a valid receive-buffer upper bound,
because mandatory protocol structure can require a larger datagram.
--- ---
## 10. Sequence Handling and Received Statistics ## 10. Sequence Handling and Received Statistics
6
@@ -1132,10 +1338,31 @@ seq 3 payload: 74 74 69 72 74 74 69
and across separate sessions on the same listener, the phase carried over. and across separate sessions on the same listener, the phase carried over.
**The continuity extends across listeners, not only across sessions on one
listener.** In the tested configuration — one server bound to both an IPv4 and an
IPv6 listener — successive echo replies drawn from a session on each continued
the same repeating pattern without resetting. So the observable phase is
continuous across the sessions tested and across those two listeners.
That is the extent of what was measured. It is not established here that a single
stream is shared by *every* session of a process in every configuration, and no
claim is made about how any implementation produces the bytes.
**Protocol requirement.** A client MUST NOT assume a reply payload begins at the **Protocol requirement.** A client MUST NOT assume a reply payload begins at the
start of the fill pattern, and MUST NOT use payload bytes for any protocol start of the fill pattern, and MUST NOT use payload bytes for any protocol
purpose. A compatible server MAY reset the phase per packet. purpose. A compatible server MAY reset the phase per packet.
**No recommendation follows from the phase observation alone.** The default fill
is a fixed, public pattern, so its phase carrying across sessions discloses
nothing about another peer's traffic — unlike the no-fill mode of Section 13.4,
which is a genuine disclosure risk and carries its own requirement there. A
compatible server may reset the phase per packet, continue it, or derive the
payload some other way. The resulting payload bytes can differ, making the
phase policy observable in a packet capture, but the difference has no protocol
significance: these choices are interoperability-equivalent because a
conforming client cannot depend on payload phase. This specification prescribes
no particular arrangement of fill state.
### 13.4 No Fill ### 13.4 No Fill
**Upstream 0.9.1 behavior.** When the effective fill is `none`, no bytes are **Upstream 0.9.1 behavior.** When the effective fill is `none`, no bytes are
@@ -1251,7 +1478,7 @@ and no payload.
| Trailing bytes after the token | Accepted; session removed | | Trailing bytes after the token | Accepted; session removed |
| Repeated close with the same token | Dropped as an unknown token; no reply | | Repeated close with the same token | Dropped as an unknown token; no reply |
| Echo after close | Dropped as an unknown token; no reply | | Echo after close | Dropped as an unknown token; no reply |
| Close from a foreign source port | Dropped; session **not** removed | | Close from a foreign source port | Dropped; does not remove an otherwise live, non-expired session. See Section 18.2 for the separately observed expired-session release case. |
| Close bearing an unknown or zero token | Dropped; no reply | | Close bearing an unknown or zero token | Dropped; no reply |
**Protocol requirement.** A server MUST accept the close request and release the **Protocol requirement.** A server MUST accept the close request and release the
2
@@ -1294,10 +1521,16 @@ duration hard limit:
- when a maximum test duration is configured, the server clamps the negotiated - when a maximum test duration is configured, the server clamps the negotiated
duration to it during negotiation; duration to it during negotiation;
- it additionally enforces a hard deadline of **maximum duration + 2 seconds**, - it additionally enforces a hard deadline of **maximum duration + 2 seconds**,
measured from the **first echo request** of the session; measured from the session's **first echo request that is actually served**;
- the first echo request arriving after that deadline is answered with a normal, - the first echo request arriving after that deadline **that would otherwise be
complete echo reply — correct sequence number, token, statistics and served** is answered with a normal, complete echo reply — correct sequence
timestamps — with the Close flag additionally set; number, token, statistics and timestamps — with the Close flag additionally
set;
- in the one tested dropped-trigger case, a deadline-crossing echo that was
rate-limited was silently dropped without Close. The session remained usable,
and the next served echo carried Close;
- the corresponding behavior for other independently dropped post-deadline
request classes was not tested and is not specified here;
- the session is removed at the same time, so every later request is dropped as - the session is removed at the same time, so every later request is dropped as
an unknown token. an unknown token.
@@ -1306,6 +1539,43 @@ first echo; maximum duration 2 s → close flag at 4.06 s and again at 4.1 s in
separate run. Captured in `captures/server-close.pcapng` (frame 36 carries separate run. Captured in `captures/server-close.pcapng` (frame 36 carries
flags `0x06`). flags `0x06`).
Re-measured as a boundary sweep, the 2-second margin is additive across the
configured maximum rather than proportional to it. Offsets are measured from the
first served echo:
| Configured maximum duration | Last offset still answered normally | First offset carrying Close |
|-----------------------------|-------------------------------------|-----------------------------|
| 500 ms | 2.45 s | 2.50 s |
| 1 s | 2.95 s | 3.00 s |
| 3 s | 4.95 s | 5.00 s |
Four further properties of the trigger were measured, and each is externally
visible:
- **The clock starts at the first *served* echo, not at open and not at the first
echo that merely arrives.** Opening a session five seconds before its first
echo did not move the deadline. Two drop classes were tested as the first
request and neither started the clock: an **oversized** first request, and a
first request from a **foreign source endpoint**. In both cases a later served
echo became the origin instead. Whether the other drop classes of Section 5.4
behave the same way was not tested, and no general rule about them is stated
here.
- **The triggering echo is included in the reception statistics it reports.** The
close-flagged reply carries the count and window that request produced, exactly
as an ordinary reply would.
- **A request that would trigger the close but is itself rate-limited is simply
dropped.** The close is not lost: it is carried by the next echo that is served
once the rate allowance refills.
- **The token is unusable from that reply onward.** The client's next echo, and
the close request it sends in response to the flag, both go unanswered.
The close-flagged reply itself is captured in `captures/server-close.pcapng`,
frame 36; the sweep above is a timing measurement and has no capture of its own.
The margin is a property of the tested builds, stated with a measurement
tolerance of one probe interval (50 ms in the sweep above). It is upstream
policy, not a protocol constant, and none of the protocol requirements below
depends on its value.
A conforming client never reaches this deadline, because it stops sending at the A conforming client never reaches this deadline, because it stops sending at the
negotiated duration. The mechanism exists to bound sessions from clients that negotiated duration. The mechanism exists to bound sessions from clients that
ignore the negotiated duration. ignore the negotiated duration.
@@ -1356,6 +1626,7 @@ token**, and the fully restricted parameter payload.
| Subsequent close with the zero token | Dropped, no reply | | Subsequent close with the zero token | Dropped, no reply |
chatgpt-codex-connector[bot] commented 2026-08-12 09:01:37 +00:00 (Migrated from github.com)
Review

P2 Badge Scope reclamation to the tested open burst

The admitted experiment only shows that a burst of ordinary opens caused the expired session to disappear before the subsequent probe; it does not establish that one ordinary open always reclaims an expired session rather than reclamation depending on a threshold or another batch condition. This row turns that batch outcome into a per-request invariant, so describe only the tested burst or leave single-open behavior ambiguous.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope reclamation to the tested open burst** The admitted experiment only shows that a burst of ordinary opens caused the expired session to disappear before the subsequent probe; it does not establish that one ordinary open always reclaims an expired session rather than reclamation depending on a threshold or another batch condition. This row turns that batch outcome into a per-request invariant, so describe only the tested burst or leave single-open behavior ambiguous. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/3f02372f5bb3b55c726ecda7863f04c344838157/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 09:04:19 +00:00 (Migrated from github.com)
Review

Agreed — only the burst was tested. §18.2 now reads "A burst of ordinary opens released an expired session; a burst of no-test opens did not", and states that whether a single open suffices, or whether a count/rate matters, was not measured. The §16 row now compares burst with burst rather than implying a per-open invariant.
🤖 Addressed by Claude Code

Agreed — only the burst was tested. §18.2 now reads "A burst of ordinary opens released an expired session; a burst of no-test opens did not", and states that whether a single open suffices, or whether a count/rate matters, was not measured. The §16 row now compares burst with burst rather than implying a per-open invariant. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
| With authentication | Reply is authenticated normally; flags `0x0F` | | With authentication | Reply is authenticated normally; flags `0x0F` |
| Cleanup required | None | | Cleanup required | None |
| Effect on *other* sessions' expiry | **None observed.** A burst of no-test opens did not release an unrelated expired session, while a burst of ordinary opens did (Section 18.2) |
**Protocol requirement.** A server MUST NOT create a usable session for a **Protocol requirement.** A server MUST NOT create a usable session for a
no-test open, and MUST return a zero token. The Close flag in the reply here no-test open, and MUST return a zero token. The Close flag in the reply here
@@ -1367,8 +1638,10 @@ specification, Sections 6.2 and 8.3.
## 17. Malformed, Unexpected and Adversarial Packets ## 17. Malformed, Unexpected and Adversarial Packets
Compact reference table. "Session effect: none" means the referenced session (if Compact reference table. For rows whose session state is **live**, "Session
any) is left fully usable. effect: none" means the referenced session remains usable. The separately
observed expired-session release cases are scoped to the **expired** row and
Section 18.2.
| Request condition | Session state | Auth state | Server response | Session effect | | Request condition | Session state | Auth state | Server response | Session effect |
|---|---|---|---|---| |---|---|---|---|---|
2
@@ -1399,8 +1672,8 @@ any) is left fully usable.
| Echo shorter than the negotiated length | live | ok | **reply at negotiated length** | normal | | Echo shorter than the negotiated length | live | ok | **reply at negotiated length** | normal |
| Echo with a duplicate sequence number | live | ok | **reply** | count +1, window unchanged | | Echo with a duplicate sequence number | live | ok | **reply** | count +1, window unchanged |
| Echo with a lower sequence number than the last | live | ok | **reply** | count +1, window reset to `0x1` | | Echo with a lower sequence number than the last | live | ok | **reply** | count +1, window reset to `0x1` |
| Echo after the maximum-duration deadline | live | ok | **reply with Close flag** | session removed | | Otherwise-serviceable echo after the maximum-duration deadline | live | ok | **normal echo reply with Close flag** | request counted; session removed (a rate-limited trigger is dropped; see Section 15.2) |
| Echo after idle expiry deadline | expired | ok | **reply** (served once) | session removed | | Otherwise-serviceable echo that is the first tested packet to touch an expired session | expired | ok | **reply** (served once), then session removed | see Section 18.2 for normally dropped packets that can release it first |
| Any request on a closed / expired session | gone | ok | drop | none | | Any request on a closed / expired session | gone | ok | drop | none |
| Close with trailing bytes | live | ok | **accepted**; no reply, as for any close | session removed | | Close with trailing bytes | live | ok | **accepted**; no reply, as for any close | session removed |
| Echo-shaped datagram with the Open flag set, otherwise admissible | — | ok | treated as an **open**; the token bytes are parsed as parameters | new session if those bytes parse | | Echo-shaped datagram with the Open flag set, otherwise admissible | — | ok | treated as an **open**; the token bytes are parsed as parameters | new session if those bytes parse |
@@ -1439,7 +1712,7 @@ default **1 minute**, with `0` meaning "never expire".
**Observed behavior**, measured with a 2-second idle timeout: **Observed behavior**, measured with a 2-second idle timeout:
| Gap since the previous accepted echo | First request after the gap | The request after that | | Gap since the previous accepted echo | First otherwise-serviceable echo after the gap | Immediate follow-up |
|---|---|---| |---|---|---|
| 5.0 s | answered | answered | | 5.0 s | answered | answered |
chatgpt-codex-connector[bot] commented 2026-08-12 07:13:42 +00:00 (Migrated from github.com)
Review

P2 Badge Do not classify post-idle behavior as unobservable

When the client-to-server path is interrupted for longer than idle timeout + 5 s but recovers before a longer negotiated test duration ends, a conforming client does enter this state: Section 18.2 says upstream answers its first otherwise-serviceable echo after recovery, whereas an implementation using either permitted alternative here remains silent. Those are observably different outcomes, so the claim that no conforming client can tell and that every outcome is silence cannot justify making the behavior unrestricted; account for mid-test packet loss or narrow the classification accordingly.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Do not classify post-idle behavior as unobservable** When the client-to-server path is interrupted for longer than `idle timeout + 5 s` but recovers before a longer negotiated test duration ends, a conforming client does enter this state: Section 18.2 says upstream answers its first otherwise-serviceable echo after recovery, whereas an implementation using either permitted alternative here remains silent. Those are observably different outcomes, so the claim that no conforming client can tell and that every outcome is silence cannot justify making the behavior unrestricted; account for mid-test packet loss or narrow the classification accordingly. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/11e8a9bf3fcffa6dd9ceb5ed4cd7fcf5c6144ee8/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 07:28:53 +00:00 (Migrated from github.com)
Review

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. The false “no conforming client can tell” claim was removed; a conforming client can observe the difference after sufficiently long mid-test client-to-server path loss and recovery. This was not promoted to a new unsupported RFC 2119 MUST; the clean implementation policy remains open.

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. The false “no conforming client can tell” claim was removed; a conforming client can observe the difference after sufficiently long mid-test client-to-server path loss and recovery. This was not promoted to a new unsupported RFC 2119 MUST; the clean implementation policy remains open.
| 6.5 s | answered | answered | | 6.5 s | answered | answered |
@@ -1450,14 +1723,96 @@ default **1 minute**, with `0` meaning "never expire".
Two things follow, and both are externally observable: Two things follow, and both are externally observable:
1. **The effective expiry deadline is the configured idle timeout plus a 1. **The observed idle release boundary is the configured idle timeout plus a
5-second grace period.** Measured boundary between 6.9 s and 7.1 s for a 5-second grace period.** Measured boundary between 6.9 s and 7.1 s for a
2-second timeout. 2-second timeout.
2. **The first request that arrives after the deadline is still answered**, and 2. **In observed upstream behavior, the first otherwise-serviceable echo that
the session is torn down as part of handling it. Only the *next* request is reaches an expired session receives a final reply.** The immediate follow-up
dropped. A compatible implementation MAY drop the first post-deadline request is then dropped, which is how release is observed externally.
instead; clients cannot depend on either behavior, because from their side
the deadline coincides with an unresponsive server. The qualification matters, and the paragraphs below are where it comes from.
A final post-deadline reply is emitted only if the packet that first triggers
the expired session's release is itself an otherwise-serviceable echo. If a
packet that would normally be dropped — a foreign-endpoint echo, a truncated
or oversized one — reaches the expired session first, it may release the
session without receiving a reply; no later request then receives that final
reply.
**The grace is a fixed additive margin, not a proportion of the timeout.** A
boundary sweep across three configured timeouts, three repetitions each, located
the observed session-release boundary at timeout + 5 s in every case, to within
the 50 ms probe spacing. Each probe used a first otherwise-serviceable echo after
the gap and an immediate follow-up:
| Configured idle timeout | Longest gap with follow-up served | Shortest gap with follow-up dropped |
|-------------------------|----------------------------------|------------------------------------|
| 500 ms | 5.45 s | 5.50 s |
| 2 s | 6.95 s | 7.00 s |
| 4 s | 8.95 s | 9.00 s |
At and beyond this boundary, the first otherwise-serviceable post-gap echo may
still receive upstream's final lazy-release reply; the immediate follow-up is
then dropped. These values are therefore not thresholds at which the first
post-gap echo is rejected.
The same boundary was observed on the 0.9.0 release, the 0.9.1 release and the
tested post-0.9.1 development build. Because the extra margin is the same five
seconds at every timeout, it is not proportional to the timeout, and the boundary
did not shift with when the session was opened. Nothing here describes how a
server notices that a session has expired; only when it stops serving one is
observable, and only that is stated.
**Expiry is observable as a lazy release.** A final post-deadline reply is
emitted only when the packet that first triggers the expired session's release is
itself otherwise serviceable; it is not reserved for a later request from the
session's client. Measured against a control in the same capture
(`captures/server-expiry-consumption.pcapng`):
- **Control.** A session left idle past its deadline answered the client's next
echo, then dropped the one after it.
- **Released by a foreign endpoint.** A second session, otherwise identical, was
touched just past its deadline by an echo bearing its token from a *different
source port*. That packet was dropped as a foreign-endpoint request, as it
would be at any time — but it released the session without a reply. The
legitimate client's own post-deadline echo, arriving 0.3 s later, then received
**no reply at all**, and neither did the one after it; no final reply remained.
The same effect was produced by a truncated echo, an oversized echo, a close
request from a foreign endpoint, and a burst of unrelated ordinary opens arriving
before the legitimate client's post-deadline request.
Two limits on how far this generalises. First, these are the request classes that
were tested; this specification does not claim that *every* packet touching an
expired token has the effect, only these. Second, it says nothing about the order
in which a server performs any internal step — what is observable is that the
session was gone by the time the legitimate request arrived, and that in the
control, with no intervening packet, it was not.
**A burst of ordinary opens released an expired session; a burst of no-test opens
did not.** A burst of normal open requests was observed to release an unrelated
expired session, so a session can also stop being served without any packet
bearing its token arriving at all. A burst of no-test opens (Section 16) had no
such effect — consistent with a no-test open creating no session of its own.
Only the burst was tested. Whether a **single** ordinary open is enough, or
whether the effect depends on a count, a rate, or some other property of the
batch, was not measured and is not claimed here.
> **Black-box inference.** Taken together, these observations are consistent with
> expiry being evaluated when a session is looked up and, additionally, when a new
> session is created, rather than by a scheduled removal that a client could
> observe as a distinct event. That is an inference from the timing and packet
> outcomes above, not a directly measured description of the mechanism, and no
> requirement in this document depends on it.
**Compatibility significance.** The lazy-release behavior is externally
observable. A conforming client may encounter it after a sufficiently long
mid-test client-to-server path interruption and observe whether the first
otherwise-serviceable echo after recovery receives a reply.
**Clean policy remains open.** Whether a clean server reproduces upstream lazy
release or expires immediately is a separate implementation decision. The
current evidence does not introduce a new RFC 2119 requirement for that choice.
**Upstream 0.9.1 behavior — a session that has never carried an echo request **Upstream 0.9.1 behavior — a session that has never carried an echo request
never expires.** The idle clock starts at the first echo request, not at open. never expires.** The idle clock starts at the first echo request, not at open.
@@ -1476,7 +1831,8 @@ them resident indefinitely as upstream does.
Requests dropped for rate limiting still refresh the idle clock, so a client Requests dropped for rate limiting still refresh the idle clock, so a client
chatgpt-codex-connector[bot] commented 2026-08-12 04:51:52 +00:00 (Migrated from github.com)
Review

P2 Badge Measure duration from the first served echo everywhere

When an oversized or foreign-endpoint request arrives before the first accepted echo, this new rule starts the maximum-duration clock at the later served echo, but the companion client specification still states at IRTT_CLIENT_PROTOCOL_SPEC.md:1520-1523 that it starts at the session's first echo request. Since both documents are compatibility baselines, update the client specification with this refinement so implementers do not receive conflicting lifecycle rules.

AGENTS.md reference: AGENTS.md:L18-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Measure duration from the first served echo everywhere** When an oversized or foreign-endpoint request arrives before the first accepted echo, this new rule starts the maximum-duration clock at the later served echo, but the companion client specification still states at `IRTT_CLIENT_PROTOCOL_SPEC.md:1520-1523` that it starts at the session's first echo request. Since both documents are compatibility baselines, update the client specification with this refinement so implementers do not receive conflicting lifecycle rules. AGENTS.md reference: [AGENTS.md:L18-L23](https://github.com/Lochnair/irtt-rs/blob/b3b754075b850acc6b26dfa36d9ad24ced0a588f/AGENTS.md#L18-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 05:15:39 +00:00 (Migrated from github.com)
Review

Agreed — carried the refinement into IRTT_CLIENT_PROTOCOL_SPEC.md §19.3 so both compatibility baselines state the origin as the first served echo, with a dated note that the earlier measurements are unaffected (their first request was served), and recorded the change in a new "Part IV Client Spec Change Applied" table.
🤖 Addressed by Claude Code

Agreed — carried the refinement into `IRTT_CLIENT_PROTOCOL_SPEC.md` §19.3 so both compatibility baselines state the origin as the first *served* echo, with a dated note that the earlier measurements are unaffected (their first request was served), and recorded the change in a new "Part IV Client Spec Change Applied" table. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
sending faster than the allowance keeps its session alive without receiving sending faster than the allowance keeps its session alive without receiving
replies. replies. Section 9.3 records the full drop-class comparison establishing this,
and that no other tested drop class has the same effect.
chatgpt-codex-connector[bot] commented 2026-08-12 04:51:52 +00:00 (Migrated from github.com)
Review

P2 Badge Correct the DSCP trial to three rounds

The admitted capture contains three echo rounds per session, not four: its 32 packets comprise eight open packets plus 24 packets for three request/reply rounds. The verification report and behavioral vectors likewise explicitly say three rounds and label them rounds 1–3, so claiming all four rounds overstates the recorded evidence.

AGENTS.md reference: AGENTS.md:L26-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Correct the DSCP trial to three rounds** The admitted capture contains three echo rounds per session, not four: its 32 packets comprise eight open packets plus 24 packets for three request/reply rounds. The verification report and behavioral vectors likewise explicitly say three rounds and label them rounds 1–3, so claiming all four rounds overstates the recorded evidence. AGENTS.md reference: [AGENTS.md:L26-L30](https://github.com/Lochnair/irtt-rs/blob/b3b754075b850acc6b26dfa36d9ad24ced0a588f/AGENTS.md#L26-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 05:15:42 +00:00 (Migrated from github.com)
Review

Correct, and it was inconsistent with my own vectors and Part IV, which both said three. Verified against the capture (32 packets = 8 open + 3 rounds × 8) and changed the spec to "three interleaved rounds ... in all three rounds".
🤖 Addressed by Claude Code

Correct, and it was inconsistent with my own vectors and Part IV, which both said three. Verified against the capture (32 packets = 8 open + 3 rounds × 8) and changed the spec to "three interleaved rounds ... in all three rounds". _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
### 18.3 Resource Limits ### 18.3 Resource Limits
4
@@ -1538,9 +1894,9 @@ implementation.
| Default minimum send interval | 10 ms | policy | | Default minimum send interval | 10 ms | policy |
| Default burst allowance | 5 requests | policy | | Default burst allowance | 5 requests | policy |
| Default idle timeout | 1 minute | policy | | Default idle timeout | 1 minute | policy |
| Idle-expiry grace beyond the timeout | 5 seconds (measured) | policy | | Idle-expiry grace beyond the timeout | 5 seconds, additive (measured at timeouts of 500 ms, 2 s and 4 s) | policy |
| Maximum negotiable interval | idle timeout ÷ 4 (measured) | policy | | Maximum negotiable interval | idle timeout ÷ 4 (measured) | policy |
| Maximum-duration grace | 2 seconds (measured) | policy | | Maximum-duration grace | 2 seconds, additive, from the first served echo (measured at maxima of 500 ms, 1 s and 3 s) | policy |
| Default maximum test duration | unlimited | policy | | Default maximum test duration | unlimited | policy |
| Default maximum packet length | unlimited | policy | | Default maximum packet length | unlimited | policy |
| Default fill pattern | repeating `69 72 74 74` | policy | | Default fill pattern | repeating `69 72 74 74` | policy |
2
@@ -1558,11 +1914,11 @@ would produce a smaller interval makes the server unusable by those clients.
**Protocol requirement — what the wire parameter is.** The DSCP parameter **Protocol requirement — what the wire parameter is.** The DSCP parameter
(tag 8) carries the **raw IP TOS / Traffic Class byte**, with a useful range of (tag 8) carries the **raw IP TOS / Traffic Class byte**, with a useful range of
**0–255**. It is not a 6-bit codepoint. A server applies the negotiated value **0–255**. It is not a 6-bit codepoint. For negotiated values in that range, a
directly as the TOS / Traffic Class byte of its echo replies, and a compatible server applies the value directly as the TOS / Traffic Class byte of its echo
implementation MUST encode and interpret it the same way. The common Expedited replies, and a compatible implementation MUST encode and interpret it the same
chatgpt-codex-connector[bot] commented 2026-08-12 08:24:41 +00:00 (Migrated from github.com)
Review

P2 Badge Distinguish DSCP application failures from send failures

For an out-of-range DSCP value, these lines call the missing replies send failures while also establishing that the server continues serving its other sessions. Section 6.3, however, says a non-transient reply send failure terminates the listener and whole process. Black-box evidence supports only that these replies were absent and unrelated sessions stayed healthy; it cannot identify whether applying the marking failed before transmission or the send itself failed. State the observable outcome and leave the failure stage ambiguous, or explicitly distinguish this error class from the fatal send failures.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Distinguish DSCP application failures from send failures** For an out-of-range DSCP value, these lines call the missing replies send failures while also establishing that the server continues serving its other sessions. Section 6.3, however, says a non-transient reply send failure terminates the listener and whole process. Black-box evidence supports only that these replies were absent and unrelated sessions stayed healthy; it cannot identify whether applying the marking failed before transmission or the send itself failed. State the observable outcome and leave the failure stage ambiguous, or explicitly distinguish this error class from the fatal send failures. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/064b7d550107c7e4d10cd96c93796c8d2731e07b/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:47:07 +00:00 (Migrated from github.com)
Review

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. The docs now record only the observable result for DSCP >=256: OPEN succeeds, the affected session emits no observed ECHO reply, and unrelated sessions remain healthy. The exact failure stage is explicitly left unknown and is no longer called a verified send failure; it is distinguished from the fatal outbound send failure.

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. The docs now record only the observable result for DSCP >=256: OPEN succeeds, the affected session emits no observed ECHO reply, and unrelated sessions remain healthy. The exact failure stage is explicitly left unknown and is no longer called a verified send failure; it is distinguished from the fatal outbound send failure.
Forwarding marking is therefore **0xb8 (184)** on the wire, not 46. Verified by way. The common Expedited Forwarding marking is therefore **0xb8 (184)** on the
capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and wire, not 46. Verified by capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
`captures/dscp-session.pcapng`. `captures/dscp-session.pcapng`.
> **Out of scope: user-facing DSCP notation.** Whether an implementation's > **Out of scope: user-facing DSCP notation.** Whether an implementation's
chatgpt-codex-connector[bot] commented 2026-08-12 06:28:09 +00:00 (Migrated from github.com)
Review

P2 Badge Qualify the compact maximum-duration row

When the deadline-crossing echo is rate-limited, this new rule drops it and defers Close until a later served echo, but Section 17's compact adversarial table still says unconditionally that an “Echo after the maximum-duration deadline” receives a close-flagged reply and removes the session. Fresh evidence is that the still-unqualified row remains at Section 17 line 1632; qualify it as otherwise serviceable or add the rate-limited exception there.

AGENTS.md reference: AGENTS.md:L18-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Qualify the compact maximum-duration row** When the deadline-crossing echo is rate-limited, this new rule drops it and defers Close until a later served echo, but Section 17's compact adversarial table still says unconditionally that an “Echo after the maximum-duration deadline” receives a close-flagged reply and removes the session. Fresh evidence is that the still-unqualified row remains at Section 17 line 1632; qualify it as otherwise serviceable or add the rate-limited exception there. AGENTS.md reference: [AGENTS.md:L18-L23](https://github.com/Lochnair/irtt-rs/blob/2ed0e2570ea18b118e78d9f4f196d0051e4963e7/AGENTS.md#L18-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 06:49:50 +00:00 (Migrated from github.com)
Review

Addressed in 389c26b. The compact row now requires an otherwise-serviceable ECHO, specifies the normal Close-flagged reply and counted request, and names the rate-limited trigger exception.

Addressed in 389c26b. The compact row now requires an otherwise-serviceable ECHO, specifies the normal Close-flagged reply and counted request, and names the rate-limited trigger exception.
2
@@ -1573,28 +1929,108 @@ capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
> upstream wire behavior, and it does not belong in this specification. > upstream wire behavior, and it does not belong in this specification.
chatgpt-codex-connector[bot] commented 2026-08-12 09:11:25 +00:00 (Migrated from github.com)
Review

P2 Badge Remove clamping from the indistinguishable mappings

Clamping -1 to the byte range 0..=255 produces 0, not 255, so the observed IPv4 mapping actually distinguishes ordinary clamping from modulo-256 conversion. Keeping clamping in this list makes the stated ambiguity mathematically incorrect; remove it or define the different conversion that was intended.

AGENTS.md reference: AGENTS.md:L12-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Remove clamping from the indistinguishable mappings** Clamping `-1` to the byte range `0..=255` produces `0`, not `255`, so the observed IPv4 mapping actually distinguishes ordinary clamping from modulo-256 conversion. Keeping clamping in this list makes the stated ambiguity mathematically incorrect; remove it or define the different conversion that was intended. AGENTS.md reference: [AGENTS.md:L12-L14](https://github.com/Lochnair/irtt-rs/blob/7f5d2525201b8c3ff35ea4cbc7526639a6e43401/AGENTS.md#L12-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 09:14:04 +00:00 (Migrated from github.com)
Review

You are right and my arithmetic was wrong — clamping −1 into 0..=255 gives 0, so the observation excludes clamping rather than leaving it ambiguous. The candidates are now the low byte of the two's-complement representation (equivalently the value modulo 256) and a special case for this input, and the text records that plain clamping is ruled out because it would have emitted 0.
🤖 Addressed by Claude Code

You are right and my arithmetic was wrong — clamping −1 into `0..=255` gives 0, so the observation excludes clamping rather than leaving it ambiguous. The candidates are now the low byte of the two's-complement representation (equivalently the value modulo 256) and a special case for this input, and the text records that plain clamping is ruled out because it would have emitted 0. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
**Server policy.** When DSCP is permitted and supported on the socket, the server **Server policy.** When DSCP is permitted and supported on the socket, the server
applies the negotiated value to its echo replies. When DSCP is disallowed, the applies an in-range negotiated value to its echo replies. When DSCP is disallowed,
negotiated value is forced to 0 and returned as absent from the parameter the negotiated value is forced to 0 and returned as absent from the parameter
payload. payload.
**Observed range handling.** The negotiated value is **not** range-checked **Observed range handling on the tested host.** Values in `0..=255` were applied
during negotiation. Values 0–255 are applied normally. A value above 255 is normally. A value of 256 or above was accepted and echoed back in the open reply,
accepted and echoed back in the open reply, but then every echo reply for that but no echo replies were observed for that session. Other sessions continued to
session fails to send and is dropped — the client sees a session that opens and be served normally throughout.
then answers nothing.
The black-box observation does not identify whether the absence arose while
applying the requested socket marking, during transmission, or at another stage.
It is distinct from the non-transient outbound reply send failure of Sections 6.3
and 22.1, whose observed result is process termination.
**Observed marking scope, measured on interleaved sessions.** Four sessions were
opened on one listener negotiating 46, 8, 0 and 184, and then driven in three
interleaved rounds of one echo each. Every echo reply carried its own session's
value as the TOS byte, verbatim, in all three rounds, and no reply carried
another session's value. The marking therefore follows the session, not the
listener or the most recent negotiation. Captured in
`captures/server-dscp-interleaved.pcapng`.
**Observed marking of the other packet kinds.**
| Packet | Marking |
|--------|---------|
| Open reply | **Always unmarked** (TOS 0) |
| Echo reply | The session's negotiated value, verbatim for values in `0..=255` (see the out-of-range observations below) |
| Server-initiated close (Section 15.2) | Measured to carry the same session marking for values in `0..=255`. No admitted capture shows this — see the note below |
| Client-initiated close | No reply is sent at all (Section 15.1) |
An unmarked open reply is not merely the state of a freshly started listener: in
`captures/dscp-values.pcapng` a listener that had already emitted a marked echo
reply answered a subsequent open on the same port with TOS 0 (frame 4 carries
TOS `0x08`; the open reply at frame 7 is unmarked). The interleaved capture above
does not show this ordering — all four of its open exchanges precede its first
marked echo reply — so the two are cited separately.
**The close-reply row rests on a measurement, not on a capture.** No capture in
`captures/` shows a close-flagged reply on a session with a nonzero negotiated
DSCP. The interleaved capture contains no close-flagged reply, and
`captures/server-close.pcapng`, which does contain one at frame 36, is TOS 0
throughout because that session negotiated no DSCP. Treat the row as the observed
result it is, and not as a deduction from the Close flag being "just a flag on an
ordinary echo reply" — nothing here rules out a marking difference on that path
beyond the measurement itself. A capture of a marked session reaching its
maximum-duration deadline would settle it and was not taken.
**Observed handling of negative and out-of-range DSCP values on the tested host.**
A negative value behaved differently by address family. One negative value was
measured with its emitted marking recorded: a negotiated **−1** appeared on the
wire as **TOS 255** on IPv4, while the same negotiation on IPv6 produced a
traffic class of **0**. A value of 256 or above produced the no-reply result
described above.
A single negative data point does not identify the transformation. Taking the low
byte of the value's two's-complement representation — equivalently, the value
modulo 256 — and a special case for this input both predict TOS 255, and no
experiment distinguishes them. Plain clamping into `0..=255` is the one candidate
the observation does rule out: it would have emitted 0. This specification
records the observed mapping only and does not name a rule.
**This is platform behavior, not protocol behavior.** The observed handling of
negative and out-of-range values is host-specific, and a compatible server is
under no obligation to reproduce it. Do not read the v4/v6 difference as a wire
rule.
**Robustness recommendation (clean implementation).** Reject or clamp DSCP **Robustness recommendation (clean implementation).** Reject or clamp DSCP
values outside 0–255 during negotiation rather than accepting a value that cannot values outside `0..=255` during negotiation rather than accepting a negotiated
be applied, so that the value returned in the open reply is one the server will value that leads to an observed no-reply session, so that the value returned in
honour. the open reply is one the server will reliably honour. Which of the two —
rejection or clamping — and how the clean server exposes the value are decisions
for the implementing project, not settled here.
DSCP is applied to echo replies only; open and close packets are unmarked (see **What "close packets are unmarked" does and does not mean.** The client
the client specification, Section 10.8). specification, Section 10.8, records that open and close packets carry TOS 0.
That statement is about **standalone** open and close datagrams — the client's
open request, the server's open reply, and the client's close request — and it
holds: each was observed unmarked regardless of the negotiated value.
It does **not** cover a server-initiated close, because there is no standalone
close datagram in that direction. A server signals close by setting the Close
flag on an ordinary echo reply (Section 15.2), and that reply carries the
session's in-range negotiated marking like every other echo reply. A server must
not clear the marking merely because the Close flag is set, and a clean
implementation should not read "close packets are unmarked" as applying to it.
--- ---
## 21. Version Differences ## 21. Version Differences
Only one wire-visible difference between the tested builds has been found, and it
is the subject of Section 21.1. The behaviors most likely to be mistaken for
version-specific quirks were re-measured against all three builds — the 0.9.0
release, the 0.9.1 release and the tested post-0.9.1 development tree — and were
**identical** on each: the accepted-then-fatal open of Section 22.5; the idle
boundary at timeout + 5 s; the maximum-duration close at maximum + 2 s; a
rate-limited request refreshing the idle lifetime; the interface-MTU effective
length boundary; the burst allowance; and fill-phase continuity across the tested
listeners. Where a later section names a single build, that is because only that
build was measured for the statement in question.
### 21.1 0.9.0 → 0.9.1 ### 21.1 0.9.0 → 0.9.1
Two independent things are recorded here and must not be conflated. Two independent things are recorded here and must not be conflated.
4
@@ -1678,9 +2114,18 @@ reproduce. They are listed because each is reachable from ordinary client
traffic, so a clean server has to decide what to do instead. traffic, so a clean server has to decide what to do instead.
Everything in this section is an **observation plus a robustness Everything in this section is an **observation plus a robustness
recommendation**. None of it is an interoperability requirement: no conforming recommendation**. None of it is an interoperability requirement, in the specific
client can tell which choice a server made, because in every case the visible sense that no conforming client can *provoke* any of these outcomes: reaching
effect is either silence or an ordinary reply. them takes traffic a conforming client does not send, and a server that avoids
them is fully compatible with every existing client.
That is not the same as saying the consequences are invisible. Several of these
hazards end the server process, and Sections 22.1 and 22.5 record unrelated
conforming clients on the same process losing their sessions at that moment. Those
clients plainly observe the outcome — they just cannot cause it, and from their
side it is indistinguishable from the server going away for any other reason. The
classification rests on what a conforming client can trigger and on nothing being
required of the wire, not on a claim that the effects cannot be seen.
### 22.1 Oversized Reply Terminates the Server ### 22.1 Oversized Reply Terminates the Server
@@ -1691,12 +2136,30 @@ process exits. Observed on a host whose maximum outbound datagram size was
9216 bytes: a session negotiating 15000 bytes killed the server on its first 9216 bytes: a session negotiating 15000 bytes killed the server on its first
echo reply, and all subsequent clients received nothing. echo reply, and all subsequent clients received nothing.
Re-measured on the same host, the boundary sits at that maximum: negotiated
lengths producing replies of up to 9216 bytes were sent normally, and lengths
producing replies from roughly 9300 bytes upward were fatal on the first echo.
Those figures are that host's outbound datagram limit and its interface MTUs;
they are **not** protocol constants, and no clean implementation should encode
them. On the tested host, this failure was reachable in configurations whose
effective reply cap exceeded the host's outbound datagram ceiling, including the
tested loopback and wildcard cases. The tested 1500- and 1280-byte interface-bound
listeners could not reach it because their reply caps were lower. Jumbo interfaces
chatgpt-codex-connector[bot] commented 2026-08-12 07:37:18 +00:00 (Migrated from github.com)
Review

P2 Badge Distinguish triggering the crash from observing it

When a nonconforming peer triggers this defect while well-behaved clients have sessions on the same process, the preceding lines state that every session stops being served, so those clients can observe whether the server survived. This does not make reproducing the crash an interoperability requirement, but the rationale should say that a conforming client cannot trigger the state rather than claiming the resulting outage is unobservable.

AGENTS.md reference: AGENTS.md:L18-L23

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Distinguish triggering the crash from observing it** When a nonconforming peer triggers this defect while well-behaved clients have sessions on the same process, the preceding lines state that every session stops being served, so those clients can observe whether the server survived. This does not make reproducing the crash an interoperability requirement, but the rationale should say that a conforming client cannot *trigger* the state rather than claiming the resulting outage is unobservable. AGENTS.md reference: [AGENTS.md:L18-L23](https://github.com/Lochnair/irtt-rs/blob/cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1/AGENTS.md#L18-L23) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 07:54:39 +00:00 (Migrated from github.com)
Review

Addressed in 47bf6a2f0f563ca91365a13c4b97a67f01246fb9. Conforming clients cannot themselves trigger the malformed negotiation, but unrelated conforming clients can observe the resulting process-wide outage if another peer triggers it. Reproducing the crash remains a robustness defect, not an interoperability requirement.

Addressed in 47bf6a2f0f563ca91365a13c4b97a67f01246fb9. Conforming clients cannot themselves trigger the malformed negotiation, but unrelated conforming clients can observe the resulting process-wide outage if another peer triggers it. Reproducing the crash remains a robustness defect, not an interoperability requirement.
and other platforms were not tested (Section 23.5).
**The blast radius is the whole process, not the session or the listener.** With
a wildcard bind, a fatal send on the IPv4 listener terminated the process and
took the healthy IPv6 listener down with it — sessions with no relationship to
the offending one, on a different address family, stopped being served at the
same instant.
The default configuration places no upper bound on the negotiated length, so The default configuration places no upper bound on the negotiated length, so
this is reachable without any malformed packet. this is reachable without any malformed packet.
**Robustness recommendation (restated from Section 6.3).** Treat a per-packet **Robustness recommendation (restated from Section 6.3).** Treat a per-packet
send failure as a per-packet event rather than a fatal one, and refuse to send failure as a per-packet event rather than a fatal one, and refuse to
negotiate a length the server cannot transmit. negotiate a length the server cannot transmit. A failure encountered while
serving one session should not stop unrelated sessions or unrelated listeners.
### 22.2 Zero-Length Fill Pattern Terminates the Server ### 22.2 Zero-Length Fill Pattern Terminates the Server
2
@@ -1729,6 +2192,76 @@ leak bytes from other clients' traffic. The requirement not to emit another
peer's data is stated in Section 13.4; it is a security property of the peer's data is stated in Section 13.4; it is a security property of the
implementation rather than a wire-compatibility rule. implementation rather than a wire-compatibility rule.
### 22.5 An Accepted Open With No Clock Parameter Is Fatal On Its First Echo
**Observed upstream robustness defect.** This is the most severe of the hazards
recorded here: it is reachable in the default configuration, from two
unauthenticated datagrams, by a peer that never sends anything malformed.
When the server's restricted (effective) StampAt remains non-none, an open
request whose requested StampAt is Send, Receive, Both or Midpoint and which
**omits the Clock parameter entirely** is accepted:
- the open reply is an ordinary session-creating reply, with a valid non-zero
token and the restricted parameters;
- the reply carries **no** Clock parameter, and no Clock value is synthesized
into it;
- the **first echo request** on that session receives **no reply**, and the
chatgpt-codex-connector[bot] commented 2026-08-12 05:20:25 +00:00 (Migrated from github.com)
Review

P2 Badge Do not claim an unmeasured loopback MTU cap

The fresh Section 9.2 wording now correctly says the loopback trial only reached 8000 bytes and cannot establish a 16384-byte cap because the process terminates around 9300, but this remaining-unknown entry still says all three interfaces were capped exactly at their MTUs. That leaves the compatibility baseline internally contradictory and again overstates the admitted evidence; limit the exact-cap claim here to the Ethernet and tunnel interfaces.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Do not claim an unmeasured loopback MTU cap** The fresh Section 9.2 wording now correctly says the loopback trial only reached 8000 bytes and cannot establish a 16384-byte cap because the process terminates around 9300, but this remaining-unknown entry still says all three interfaces were capped exactly at their MTUs. That leaves the compatibility baseline internally contradictory and again overstates the admitted evidence; limit the exact-cap claim here to the Ethernet and tunnel interfaces. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/6635464d0a5039a9b125c799529c4a5272c3248b/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 05:23:54 +00:00 (Migrated from github.com)
Review

Missed this one when fixing §9.2 — §23.5 now says the exact cap holds for the Ethernet and tunnel interfaces only, records the loopback trial as a lower bound (lengths to 8000 unclamped, boundary unreachable because ~9300 ends the process), and adds "a host whose maximum outbound datagram size exceeds its loopback MTU" to what would resolve it.
🤖 Addressed by Claude Code

Missed this one when fixing §9.2 — §23.5 now says the exact cap holds for the Ethernet and tunnel interfaces only, records the loopback trial as a lower bound (lengths to 8000 unclamped, boundary unreachable because ~9300 ends the process), and adds "a host whose maximum outbound datagram size exceeds its loopback MTU" to what would resolve it. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
server process terminates. Every other session on that process, including
sessions on the other address family's listener, stops being served at the same
moment.
Reproduced for all four non-none StampAt values, and on the 0.9.0 release, the
0.9.1 release and the tested post-0.9.1 development build alike. Captured in
`captures/server-clock-absent.pcapng`: the open is answered with a token and no
Clock parameter, and the single echo request that follows is the last packet in
the exchange.
**Absence and an explicit zero are different inputs, and only absence reaches
this state.** An open encoding `Clock = 0` explicitly is out of range and is
dropped during negotiation — no reply, no session, no fatal path (Section 7.2).
It is the *omission* of the field, not a zero value, that produces a session that
is accepted and then fatal. A decoder that folds a missing tag into a zero value
cannot represent the difference and would reject the case upstream accepts, or
accept the case upstream rejects, depending on which way it folds.
**Server policy determines whether the fatal effective state exists.** A server
configured to provide no timestamps at all restricts a requested non-none
StampAt to none (Section 11.4) and answers this absent-Clock session normally,
without timestamps. A server restricted to a *single* timestamp leaves the
restricted (effective) StampAt non-none and does **not** prevent the fault.
chatgpt-codex-connector[bot] commented 2026-08-12 07:13:42 +00:00 (Migrated from github.com)
Review

P2 Badge Label the idle boundary as a session-release boundary

For the 2-second case, the preceding observed-behavior table says a request at 7.1 seconds is still answered and only the immediately following request is dropped, so the 7.00-second entry cannot literally be the shortest gap whose request is “not served.” The boundary sweep evidently uses the follow-up probe to determine that the session was released; label this column as the shortest gap after which the follow-up is dropped, or record both probe outcomes, so readers do not implement immediate expiry at the listed boundary.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Label the idle boundary as a session-release boundary** For the 2-second case, the preceding observed-behavior table says a request at 7.1 seconds is still answered and only the immediately following request is dropped, so the 7.00-second entry cannot literally be the shortest gap whose request is “not served.” The boundary sweep evidently uses the follow-up probe to determine that the session was released; label this column as the shortest gap after which the follow-up is dropped, or record both probe outcomes, so readers do not implement immediate expiry at the listed boundary. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/11e8a9bf3fcffa6dd9ceb5ed4cd7fcf5c6144ee8/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 07:29:01 +00:00 (Migrated from github.com)
Review

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. The timeout+5 values now describe the observed session-release boundary detected by the immediate follow-up probe, not a threshold at which the first post-gap request itself stops receiving a reply. The verification report and behavioral vectors use the same follow-up-based labels.

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. The timeout+5 values now describe the observed session-release boundary detected by the immediate follow-up probe, not a threshold at which the first post-gap request itself stops receiving a reply. The verification report and behavioral vectors use the same follow-up-based labels.
**Compatibility classification.** A conforming client cannot itself trigger this
defect through conforming negotiation, because conforming clients always send
Clock. If another peer triggers the resulting process-wide failure, however,
unrelated conforming clients can observe that their server disappeared and their
sessions stopped receiving replies. Reproducing the crash is not an
interoperability requirement: compatibility does not require reproducing a
robustness defect that only a nonconforming or adversarial request can trigger.
The behavior is recorded because a clean implementation has to decide what to do
instead.
**Robustness recommendation (clean implementation).** A set of session parameters
the server has already accepted at open should never be able to produce a fatal
condition when the first echo arrives; whatever else it does, a clean server
should survive this exchange with its listeners intact.
RFC 2119 keywords are deliberately **not** used here. In this document those
keywords mark interoperability requirements, and this is a robustness
recommendation rather than one. The strength of the advice is a matter for the
implementing project, which may well choose to treat it as a hard internal rule;
that decision belongs in the project's own guidance, not in an interoperability
specification.
**The specific policy is deliberately left open.** Rejecting the open, treating
the absent Clock as a defined default, restricting StampAt to none for such a
session, or some other resolution are all consistent with the recommendation
chatgpt-codex-connector[bot] commented 2026-08-12 07:13:42 +00:00 (Migrated from github.com)
Review

P2 Badge Qualify endpoint-mismatch invariants for expired sessions

When a foreign-endpoint packet arrives after the idle deadline, this new behavior says it releases the session, but Sections 8.1 and 15.1 still state without qualification that a foreign echo leaves the session intact and a foreign close does not remove it; the conformance summary likewise requires endpoint mismatches to be dropped without disturbing the session. Explicitly scope those older rules to live, non-expired sessions or cross-reference this expiry exception so implementers are not given opposite lifecycle requirements.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Qualify endpoint-mismatch invariants for expired sessions** When a foreign-endpoint packet arrives after the idle deadline, this new behavior says it releases the session, but Sections 8.1 and 15.1 still state without qualification that a foreign echo leaves the session intact and a foreign close does not remove it; the conformance summary likewise requires endpoint mismatches to be dropped without disturbing the session. Explicitly scope those older rules to live, non-expired sessions or cross-reference this expiry exception so implementers are not given opposite lifecycle requirements. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/11e8a9bf3fcffa6dd9ceb5ed4cd7fcf5c6144ee8/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 07:28:57 +00:00 (Migrated from github.com)
Review

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. Live-session endpoint-mismatch behavior remains unchanged, while the text now scopes out and cross-references the separately observed expired-session release exception in Sections 8.1, 15.1, and 17; Finding S-1 now carries the same live-session scope.

Addressed in cbd75fd6bfb3a3cdb1cad5d5fd8f9c0f99d3aed1. Live-session endpoint-mismatch behavior remains unchanged, while the text now scopes out and cross-references the separately observed expired-session release exception in Sections 8.1, 15.1, and 17; Finding S-1 now carries the same live-session scope.
chatgpt-codex-connector[bot] commented 2026-08-12 08:24:41 +00:00 (Migrated from github.com)
Review

P2 Badge Qualify the conformance summary for expired sessions

When a foreign-endpoint packet is the first to touch an idle-expired session, these lines establish that it releases the session, but Section 24.1 item 7 still requires endpoint-check failures to be dropped “without disturbing the session.” Fresh evidence after the earlier review thread is that this unconditional summary remains at lines 2367–2368 in the final tree. Scope that requirement to live, non-expired sessions or cross-reference this expiry exception so implementers are not given contradictory lifecycle rules.

AGENTS.md reference: AGENTS.md:L20-L30

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Qualify the conformance summary for expired sessions** When a foreign-endpoint packet is the first to touch an idle-expired session, these lines establish that it releases the session, but Section 24.1 item 7 still requires endpoint-check failures to be dropped “without disturbing the session.” Fresh evidence after the earlier review thread is that this unconditional summary remains at lines 2367–2368 in the final tree. Scope that requirement to live, non-expired sessions or cross-reference this expiry exception so implementers are not given contradictory lifecycle rules. AGENTS.md reference: [AGENTS.md:L20-L30](https://github.com/Lochnair/irtt-rs/blob/064b7d550107c7e4d10cd96c93796c8d2731e07b/AGENTS.md#L20-L30) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 08:47:03 +00:00 (Migrated from github.com)
Review

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. The final conformance checklist now scopes “session remains usable” to live, non-expired sessions and cross-references the separately observed expired-session release behavior in Section 18.2.

Addressed in 221c38e166983b8c6ba691e163030986ad0ce1d6. The final conformance checklist now scopes “session remains usable” to live, non-expired sessions and cross-references the separately observed expired-session release behavior in Section 18.2.
above, and each has different consequences for clients and for the
parameter-presence model. This specification does not choose among them; that is
a decision for the `irtt-rs` implementation work, to be made and recorded there.
What this section establishes is the observed upstream behavior, plus a
recommendation that any choice should satisfy.
--- ---
## 23. Remaining Unknowns ## 23. Remaining Unknowns
@@ -1807,12 +2340,20 @@ inspected, what was attempted, and what evidence would settle it.
does it vary with how the platform reports interface MTUs? does it vary with how the platform reports interface MTUs?
- **Why it matters:** It determines the largest length a server can honestly - **Why it matters:** It determines the largest length a server can honestly
negotiate. negotiate.
- **Evidence inspected:** Measured on one host: replies capped at exactly the - **Evidence inspected:** Measured on one host across three interfaces —
bind interface's MTU (1500 on an Ethernet interface); loopback with a loopback (16384), Ethernet (1500) and a tunnel (1280). On the **Ethernet and
16384-byte MTU was masked by a smaller maximum outbound datagram size. tunnel** interfaces the reply was capped at exactly that interface's MTU, byte
- **Attempted:** Two interfaces with different MTUs on one host. for byte (1281 → 1280), and a negotiated length at or below the MTU was emitted
in full. The **loopback** interface shows only that lengths up to 8000 were
emitted unclamped: its cap cannot be approached on this host, because a reply
beyond roughly 9300 bytes ends the server process first (Section 22.1), so no
16384-byte boundary was observed and none is claimed.
- **Attempted:** Three interfaces with different MTUs on one host.
- **Would resolve it:** The same measurement on Linux and Windows hosts with - **Would resolve it:** The same measurement on Linux and Windows hosts with
jumbo-frame and tunnel interfaces. jumbo-frame and tunnel interfaces. The exact-cap result rests on two interfaces
of one host and says nothing about how another platform reports interface MTUs;
a host whose maximum outbound datagram size exceeds its loopback MTU would also
settle whether the rule holds for a large-MTU interface.
### 23.6 Whether any trigger other than the duration limit sets Close on a reply ### 23.6 Whether any trigger other than the duration limit sets Close on a reply
@@ -1865,7 +2406,9 @@ in Section 24.3 and are deliberately not mixed in.
6. Return a zero token and the Close flag for a no-test open, and create no 6. Return a zero token and the Close flag for a no-test open, and create no
session for it. session for it.
7. Bind each session to its token **and** its source endpoint, and drop packets 7. Bind each session to its token **and** its source endpoint, and drop packets
that fail either check without disturbing the session. that fail either check. For a live, non-expired session, that drop leaves the
session usable from its bound endpoint; see Section 18.2 for the separately
observed expired-session release behavior.
8. Echo the token and sequence number unchanged in every echo reply. 8. Echo the token and sequence number unchanged in every echo reply.
9. Emit the negotiated fields in the specified order, and pad to the negotiated 9. Emit the negotiated fields in the specified order, and pad to the negotiated
length. For StampAt = Midpoint this means the fields the negotiated Clock length. For StampAt = Midpoint this means the fields the negotiated Clock
@@ -1898,21 +2441,28 @@ in Section 24.3 and are deliberately not mixed in.
### 24.3 Robustness recommendations for the clean implementation ### 24.3 Robustness recommendations for the clean implementation
Not interoperability requirements. No conforming client can observe whether these Not standalone interoperability requirements. They are here because each
were followed; they are here because each addresses a behavior observed upstream addresses a behavior observed upstream that a new implementation should not
that a new implementation should not copy. copy. Particular failures, including a process-wide failure caused by another
peer, can be observable to a conforming client; these recommendations do not
choose the clean implementation's specific policy.
1. Treat a per-packet send failure as a per-packet event, not a fatal one 1. Treat a per-packet send failure as a per-packet event, not a fatal one, and
(Sections 6.3, 22.1). keep a failure while serving one session from stopping unrelated sessions or
unrelated listeners (Sections 6.3, 22.1).
2. Operate under a bounded session-resource policy, and expire sessions that have 2. Operate under a bounded session-resource policy, and expire sessions that have
never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits
and eviction policy are a clean-project decision. and eviction policy are a clean-project decision.
3. Validate a fill descriptor before use, and reject a zero-length pattern 3. Validate a fill descriptor before use, and reject a zero-length pattern
(Section 22.2). (Section 22.2).
4. Reject or clamp DSCP values the socket cannot carry, during negotiation 4. Reject or clamp DSCP values outside `0..=255` during negotiation to avoid
(Section 20). accepting a value that leads to an observed no-reply session (Section 20).
5. Clamp the negotiated length to what the server can actually transmit 5. Clamp the negotiated length to what the server can actually transmit
(Sections 22.1, 22.3). (Sections 22.1, 22.3).
6. Never let a set of session parameters the server accepted at open produce a
fatal condition on a later request (Section 22.5). Which resolution to adopt
for an open that selects timestamps without a clock is deliberately left to
the implementation work.
--- ---
Binary file not shown.
@@ -2,7 +2,7 @@
**Source:** Black-box observation of an upstream IRTT **0.9.1 release** server on **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. macOS Darwin 25.5.0 arm64, driven by a raw UDP harness.
**Date:** 2026-08-11 **Date:** 2026-08-12
**Companion to:** `../IRTT_SERVER_PROTOCOL_SPEC.md` **Companion to:** `../IRTT_SERVER_PROTOCOL_SPEC.md`
These vectors are stated as **input → observed output**. They describe what a These vectors are stated as **input → observed output**. They describe what a
@@ -10,10 +10,12 @@ server was seen to emit for a given request sequence. They do not describe how
any implementation produces those bytes. any implementation produces those bytes.
Unless a row says otherwise, the server under test is the unmodified upstream 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 0.9.1 release. Two other builds appear where they are named explicitly: the 0.9.0
explicitly: the 0.9.0 release, and a build of the upstream development tree six release, and a build of the upstream development tree six upstream commits past
upstream commits past the 0.9.1 tag. Results from those builds never stand in for the 0.9.1 tag. Results from those builds never stand in for 0.9.1. Section 5.2.3
0.9.1. is a version *comparison*, where the three builds differ; elsewhere the other two
builds are named only to record that a 0.9.1 result reproduced on them unchanged
(Sections 3.2, 4.7, 8.1, 8.2).
Addresses in the accompanying captures are loopback only. Session tokens are 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 server-assigned random values and differ per run; where a token appears below it
@@ -264,6 +266,38 @@ session:
chatgpt-codex-connector[bot] commented 2026-08-12 05:20:25 +00:00 (Migrated from github.com)
Review

P2 Badge Scope deadline origin to tested drop classes

The table establishes the deadline origin only for an oversized first request and a foreign-endpoint first request; the deadline-crossing rate-limit trial does not test whether a rate-limited first request starts the clock. Generalizing this to a request dropped “for any reason” therefore turns two observations into an unsupported rule for every other admission/authentication drop class. State only the measured cases, or leave the other drop reasons ambiguous.

AGENTS.md reference: AGENTS.md:L10-L14

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Scope deadline origin to tested drop classes** The table establishes the deadline origin only for an oversized first request and a foreign-endpoint first request; the deadline-crossing rate-limit trial does not test whether a rate-limited first request starts the clock. Generalizing this to a request dropped “for any reason” therefore turns two observations into an unsupported rule for every other admission/authentication drop class. State only the measured cases, or leave the other drop reasons ambiguous. AGENTS.md reference: [AGENTS.md:L10-L14](https://github.com/Lochnair/irtt-rs/blob/6635464d0a5039a9b125c799529c4a5272c3248b/AGENTS.md#L10-L14) Useful? React with 👍 / 👎.
Lochnair commented 2026-08-12 05:23:55 +00:00 (Migrated from github.com)
Review

Agreed — only oversized and foreign-endpoint were tested in the first-request position, and the rate-limit trial is about crossing an already-running deadline, not starting one. Scoped to those two classes in the vector, spec §15.2 and Finding S-23, with the other drop classes explicitly left untested.
🤖 Addressed by Claude Code

Agreed — only oversized and foreign-endpoint were tested in the first-request position, and the rate-limit trial is about crossing an already-running deadline, not starting one. Scoped to those two classes in the vector, spec §15.2 and Finding S-23, with the other drop classes explicitly left untested. _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
The deadline is the configured maximum duration plus a 2-second grace period. The deadline is the configured maximum duration plus a 2-second grace period.
Boundary sweep, probes spaced 50 ms, offsets measured from the first **served**
echo of the session:
| configured maximum duration | last offset answered normally | first offset carrying Close |
|-----------------------------|-------------------------------|-----------------------------|
| 500 ms | 2.45 s | 2.50 s |
| 1 s | 2.95 s | 3.00 s |
| 3 s | 4.95 s | 5.00 s |
The 2-second margin is additive across the configured maximum, not proportional
to it. Reproduced on the 0.9.0 release, the 0.9.1 release and the tested
post-0.9.1 development build.
What starts and what carries the deadline:
| case | result |
|------|--------|
| Session opened 5 s before its first echo | deadline unchanged — the open does not start it |
| First request oversized (dropped by the maximum-length policy) | clock **not** started; a later served echo starts it |
| First request from a foreign source endpoint (dropped) | clock **not** started; a later served echo starts it |
| Echo that crosses the deadline | full normal reply with Close set; **counted** in the received count and window it reports |
| Echo that crosses the deadline but is itself rate-limited | dropped, no reply; the **next served** echo carries the Close flag |
| Client's next echo after the close-flagged reply | no reply — token released |
| Client's close request after the close-flagged reply | no reply — token released |
Two drop classes were tested as the session's first request — an oversized
request and one from a foreign source endpoint — and neither started the
deadline; a later served echo became the origin instead. The other drop classes
of Section 9 were not tested in that position, and this vector makes no claim
about them. The rate-limit row above concerns a request that crosses an
already-running deadline, not one that would start it.
### 3.3 Observed upstream client reaction to a close-flagged reply ### 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 Driving an upstream 0.9.1 client against a server that sets the Close flag from
@@ -360,6 +394,20 @@ 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 exactly 1 s: rejected in strict mode as an ordinary restriction, accepted in
loose mode. loose mode.
Boundary rows with a configured **minimum** interval as well. The requested
interval is raised to the minimum first and capped to (idle timeout ÷ 4)
afterwards, so the returned value can end up below the configured minimum:
| server minimum interval | server idle timeout | requested | negotiated |
|-------------------------|---------------------|-----------|------------|
| 100 ms | 1 min | 10 ms | 100 ms — raised to the minimum |
| 100 ms | 1 min | 100 ms | 100 ms — unchanged |
| 100 ms | 1 min | 60 s | 15 s — capped at timeout ÷ 4 |
| **5 s** | **4 s** | 10 ms | **1 s** — the cap wins, below the configured minimum |
| 5 s | 0 (disabled) | 10 ms | 5 s — no cap applies |
| 0 | 4 ns | 10 ms | 1 ns |
| 0 | 1 ns | 10 ms | **absent from the reply** — the cap is 0, and a restricted value of zero is omitted |
### 4.5 Timestamp allowance ### 4.5 Timestamp allowance
Requested StampAt → negotiated StampAt, by server timestamp allowance: Requested StampAt → negotiated StampAt, by server timestamp allowance:
2
@@ -372,6 +420,68 @@ Requested StampAt → negotiated StampAt, by server timestamp allowance:
| 3 (both) | 3 | **4** | 0 | | 3 (both) | 3 | **4** | 0 |
| 4 (midpoint) | 4 | 4 | 0 | | 4 (midpoint) | 4 | 4 | 0 |
### 4.6 Repeated occurrences of a known tag
One open request per row, carrying the listed occurrences of a single tag.
| parameter payload | result |
|-------------------|--------|
| Duration 1 s, then Duration 2 s | open reply, **Duration 2 s** |
| Duration 2 s, then Duration 1 s | open reply, **Duration 1 s** |
| Clock 1, then Clock 3 | open reply, **Clock 3** |
| Clock 3, then Clock 4 (out of range) | **no reply** |
| Duration 1 s, then Duration 0 (invalid) | **no reply** |
| Duration 0 (invalid), then Duration 1 s | **no reply** |
| ServerFill `rand`, then a 33-character string (invalid) | **no reply** |
Among valid duplicates, the **last** occurrence wins for every known tag tested:
ProtocolVersion, Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP
and ServerFill. Duration was additionally tested with an invalid occurrence in
both orders, and both requests were dropped. Clock and ServerFill were tested
with an invalid second occurrence, which was also dropped. The untested invalid
orders and parameters are not generalized here.
### 4.7 Absent Clock with a non-none restricted StampAt — upstream-defect vector
Capture: `../captures/server-clock-absent.pcapng`. Default-configuration server.
This vector records an **upstream robustness defect** (server specification
Section 22.5) when timestamp restriction leaves the requested non-none StampAt
non-none. It is a must-not-crash recommendation for a clean implementation, not
a behavior to reproduce.
| frame | direction | bytes | note |
|-------|-----------|-------|------|
| 1 | C → S | `14a75b01 05 06 06 06 04 00` | open; ReceivedStats = 3, StampAt = 3 (both), Length = 0, **no Clock tag** |
| 2 | S → C | `14a75b03 7ca44127b2cd7eab 01 02 05 06 06 06` | open reply, non-zero token, ProtocolVersion 1, ReceivedStats 3, StampAt 3 — **no Clock parameter** |
| 3 | C → S | `14a75b00 7ca44127b2cd7eab 00000000` | first echo, sequence 0 |
Frame 3 is the last packet in the capture. It receives no reply, and the server
process terminates; every other session on that process, on either address
family, stops being served at the same moment.
| variation | result |
|-----------|--------|
| Requested StampAt 1 (send), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
| Requested StampAt 2 (receive), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
| Requested StampAt 3 (both), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
| Requested StampAt 4 (midpoint), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
| Requested StampAt 0 (none), no Clock | open accepted, session serves normally with no timestamps |
| **StampAt 3, explicit `Clock = 0`** | **open dropped, no reply, no session** — see Section 4.2 |
| Requested StampAt 1 / 2 / 3 / 4, no Clock; no-timestamp policy restricts StampAt to none | open accepted, session serves normally with no timestamps |
| Requested StampAt 1 / 2 / 3 / 4, no Clock; single-timestamp policy leaves restricted StampAt non-none | open accepted, first echo fatal |
The last two rows are scoped to requested non-none, absent-Clock cases only:
fatality depends on the server's restricted (effective) StampAt remaining
non-none. The `StampAt 0` row is served normally whatever the server's timestamp
policy, and the explicit `Clock = 0` row is rejected during negotiation before
any policy applies; neither becomes fatal under either configuration.
Reproduced on the 0.9.0 release, the 0.9.1 release and the tested post-0.9.1
development build.
The last four rows are the point of the vector: an omitted Clock and an explicit
`Clock = 0` are different inputs with opposite upstream outcomes.
--- ---
## 5. Reply Length ## 5. Reply Length
@@ -709,8 +819,10 @@ start the idle clock, wait, then two echoes back to back.
| 7.5 s | answered | **dropped** | | 7.5 s | answered | **dropped** |
| 9.0 s | answered | **dropped** | | 9.0 s | answered | **dropped** |
Deadline = idle timeout + 5 s. The first request past the deadline is still Observed session-release boundary = idle timeout + 5 s. The first
answered, and the session is released while handling it. otherwise-serviceable request past that boundary is still answered once by
upstream, and the immediate follow-up is dropped. Section 8.3 shows what happens
when a packet that would be dropped anyway touches the token first.
**A session that has never carried an echo request does not expire.** With the **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 same 2-second timeout, a session left completely idle for 9 seconds after open
@@ -719,6 +831,98 @@ was served normally on its first echo request.
With the idle timeout disabled, a session idle for 9 seconds was served normally With the idle timeout disabled, a session idle for 9 seconds was served normally
and remained usable. and remained usable.
### 8.1 The grace is additive across configured timeouts
Session-release boundary sweep, probes spaced 50 ms, three repetitions per row,
gaps measured from the last accepted echo. Each probe used a first
otherwise-serviceable echo after the gap and an immediate follow-up; the boundary
is inferred from whether that follow-up was dropped:
| configured idle timeout | longest gap with follow-up served | shortest gap with follow-up dropped |
|-------------------------|----------------------------------|------------------------------------|
| 500 ms | 5.45 s | 5.50 s |
| 2 s | 6.95 s | 7.00 s |
| 4 s | 8.95 s | 9.00 s |
Every row sits at timeout + 5 s to within the probe spacing. At and beyond the
observed release boundary, the first otherwise-serviceable post-gap echo may
still receive a final lazy-release reply; the immediate follow-up is then
dropped. The margin is the same five seconds at each timeout, so it is additive
rather than proportional, and the boundary did not move with when the session
was opened. Reproduced on the 0.9.0 release, the 0.9.1 release and the tested
post-0.9.1 development build.
### 8.2 Which drop classes keep a session alive
Per row: open a session, send one echo (count 1), inject the listed packet, then
send another echo. The second echo's count shows whether the injected packet was
counted; separate timing trials show whether it moved the expiry boundary.
| injected packet | counted / window advanced | extends the idle lifetime |
|-----------------|---------------------------|---------------------------|
| bad magic | no | no |
| invalid flag bits | no | no |
| Reply or Open flag on a data packet | no | no |
| unknown token, or a zero token | no | no |
| valid token from a foreign source endpoint | no | no — but see 8.3 |
| echo truncated before the sequence number | no | no |
| request over the configured maximum length | no | no |
| bad, missing or zeroed MAC | no | no |
| **request with no rate allowance left** | **no** | **yes** |
The rate-limit row was separated from the alternatives three ways: a session fed
only too-fast requests outlived its idle deadline; the same cadence of oversized
requests did not; and a silent session expired on schedule. Reproduced on all
three builds. It is the only tested drop class that keeps a session alive.
### 8.3 The first packet to release an expired session determines whether a final reply is emitted
Capture: `../captures/server-expiry-consumption.pcapng`. Server idle timeout 3 s,
so the effective boundary is 8 s after the last accepted echo. Both trials are in
the one capture, which is why it is worth keeping: the control and the test differ
by a single injected packet.
Control — session on port 49865, token `fff56ec42eb8caaa`:
| frame | t | direction | packet | observed |
|-------|---|-----------|--------|----------|
| 3 | 0.00 s | C → S | echo seq 0 | reply, count 1 |
| 5 | 8.00 s | C → S | echo seq 1 — first past the deadline | **reply**, count 2 |
| 7 | 8.00 s | C → S | echo seq 2 | **no reply** — session released |
Test — session on port 53237, token `9e27590dbb016eb9`, opened at 8.70 s:
| frame | t | direction | packet | observed |
|-------|---|-----------|--------|----------|
| 10 | 8.70 s | C → S | echo seq 0 | reply, count 1 |
| 12 | 16.71 s | **X → S** | echo seq 9, correct token, **foreign source port 64688** | no reply — foreign endpoint, as at any time |
| 13 | 17.02 s | C → S | echo seq 1 — the client's first past the deadline | **no reply** |
| 14 | 17.72 s | C → S | echo seq 2 | no reply |
Frame 13 is the result. In the control the equivalent request was answered; here
the foreign-endpoint packet at frame 12 arrived 0.3 s earlier, just past the same
deadline, and the session was already gone. The foreign packet was itself dropped
and released the session without receiving a reply, so no later request received a
final reply. Whether it advanced the received count first cannot be read from this
trial — the session emitted no further reply, so no statistic reports it. The
not-counted result of Section 2 was measured on a live session, where a later
reply exposed the count, and does not carry over to this path.
The same effect was produced by a truncated echo, an oversized echo, a close
request from a foreign endpoint, and a burst of unrelated ordinary opens arriving
before the legitimate client's post-deadline request. These are the classes that
were tested; the effect is not claimed for every possible packet.
| injected before the client's post-deadline request | client's post-deadline request |
|----------------------------------------------------|--------------------------------|
| nothing (control) | **answered**, then the next one dropped |
| echo from a foreign source endpoint | **unanswered** |
| truncated echo | **unanswered** |
| oversized echo | **unanswered** |
| close from a foreign source endpoint | **unanswered** |
| burst of unrelated ordinary opens | **unanswered** |
| burst of unrelated **no-test** opens | **answered** — no-test opens create no session and reclaim none |
--- ---
## 9. Malformed Packet Admission ## 9. Malformed Packet Admission
2
@@ -768,15 +972,74 @@ therefore 184 (0xb8) in this table, not 46.
| 46 | 46 | delivered | | 46 | 46 | delivered |
| 184 | 184 | delivered | | 184 | 184 | delivered |
| 255 | 255 | delivered | | 255 | 255 | delivered |
| 256 | 256 | **all dropped** | | 256 | 256 | **no ECHO reply observed** |
| 300 | 300 | **all dropped** | | 300 | 300 | **no ECHO reply observed** |
| −5 | −5 | delivered | | −5 | −5 | delivered |
With DSCP disallowed by server policy, every requested value is returned as With DSCP disallowed by server policy, every requested value is returned as
absent (0) and replies are delivered normally. absent (0) and replies are delivered normally.
A value the server cannot apply to its socket is still accepted during An out-of-range value is still accepted during negotiation; for values of 256 or
negotiation; the session then opens successfully and answers nothing. above the session opens successfully but no ECHO reply was observed. Only that
session is affected — the server kept serving its other sessions throughout.
### 10.1 Interleaved sessions do not leak marks
Capture: `../captures/server-dscp-interleaved.pcapng`. Four sessions opened
back to back on one listener, then driven in three interleaved rounds of one echo
each. The TOS byte column is the value read from the reply's IP header.
| session (source port) | requested DSCP | open reply TOS | echo reply TOS, rounds 1–3 |
|-----------------------|----------------|----------------|-----------------------------|
| 62913 | 46 | **0x00** | 0x2e, 0x2e, 0x2e |
| 52559 | 8 | **0x00** | 0x08, 0x08, 0x08 |
| 57975 | not requested | **0x00** | 0x00, 0x00, 0x00 |
| 64688 | 184 | **0x00** | 0xb8, 0xb8, 0xb8 |
For each session in this capture, whose negotiated value is in `0..=255`, every
echo reply carries its own session's value verbatim; no reply carried another
session's value in any round. All four open replies are unmarked.
Note what this capture does **not** show: its four open exchanges are frames 1–8
and its first marked echo reply is frame 10, so no open reply here follows a
marked reply. For that ordering see Section 10.2.
### 10.2 Marking of the other packet kinds
| packet | marking |
|--------|---------|
| open reply | always TOS 0, whatever the session negotiated |
| echo reply | the session's negotiated value, verbatim for values in `0..=255` (see Section 10.3 for other values) |
| server-initiated close reply (Section 3.2) | measured to carry the same session marking for values in `0..=255`; no capture in this set shows it — the close-flagged reply in `../captures/server-close.pcapng` is from a session that negotiated no DSCP, and is TOS 0 |
| client-initiated close | no reply is sent at all |
An open reply is unmarked even on a listener that has already sent marked
replies. From `../captures/dscp-values.pcapng`, one listener serving two
sessions in sequence:
| frame | packet | TOS |
|-------|--------|-----|
| 3 | echo request, session 1 (DSCP 8) | `0x08` |
| 4 | echo reply, session 1 | `0x08` |
| 5 | close, session 1 | `0x00` |
| 6 | open request, session 2 | `0x00` |
| **7** | **open reply, session 2** | **`0x00`** |
| 8 | echo request, session 2 (DSCP 32) | `0x20` |
| 9 | echo reply, session 2 | `0x20` |
Frame 7 is the point: it follows the marked reply of frame 4 on the same
listener and is still unmarked.
### 10.3 Observed handling of negative and out-of-range DSCP values
| requested DSCP | address family | observed |
|----------------|----------------|----------|
| −1 | IPv4 | wire TOS **255** — the only negative value whose emitted marking was recorded; the transformation is not identified |
| −1 | IPv6 | traffic class **0** |
| 256 and above | either | negotiated and echoed back; no ECHO reply observed for that session |
The v4/v6 difference was observed on the tested host, not established as a wire
rule, and a compatible server is under no obligation to reproduce it.
--- ---
@@ -788,6 +1051,9 @@ negotiation; the session then opens successfully and answers nothing.
| `../captures/server-close-lifecycle.pcapng` | Client-initiated close, repeated close, and an echo after close — none answered | | `../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-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 | | `../captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, and the original endpoint continuing to be served |
| `../captures/server-clock-absent.pcapng` | An open selecting timestamps but omitting the Clock tag is answered with a token and no Clock parameter; the first echo is the last packet in the exchange (Section 4.7) |
| `../captures/server-expiry-consumption.pcapng` | Control versus foreign-endpoint-first: whether the packet that releases an expired session emits a final reply (Section 8.3) |
| `../captures/server-dscp-interleaved.pcapng` | Four interleaved sessions with distinct DSCP values, three rounds each, no cross-session leakage, unmarked open replies (Section 10.1) |
All captures are loopback traffic with the server on a fixed port and the client All captures are loopback traffic with the server on a fixed port and the client
on an ephemeral port. on an ephemeral port.
@@ -799,3 +1065,7 @@ 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** 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 altered: for each file, the full frame hexdump and the per-frame epoch timestamps
were verified byte-identical before and after the rewrite. were verified byte-identical before and after the rewrite.
The three captures added on 2026-08-12 were rewritten and verified the same way,
and their packet counts are 3, 14 and 32 respectively, unchanged from the
originals.