docs(protocol): airlock server black-box findings

Transfer independently black-box-verified server behavior from a source-assisted
research phase conducted outside this repository. Only a sanitizer-candidate
report of black-box findings and the packet captures it referenced were admitted;
no upstream source, tests, history, harnesses, build trees, result files or
source-assisted analysis crossed the boundary. Provenance and the boundary
discipline are recorded in docs/clean-room/CLEANROOM_NOTES.md.

New upstream robustness defect (Section 22.5). An open selecting timestamps while
omitting the Clock parameter is accepted with a valid token and no Clock echoed,
and the first echo on that session is fatal to the server process, taking
unrelated sessions on the same process with it. An explicit Clock=0 is instead
rejected at open, so parameter presence is externally meaningful. Recorded as an
observed defect with a must-not-crash recommendation; the clean server's
resolution is deliberately left to the implementation work.

Also established or strengthened:

- the idle grace and maximum-duration grace are fixed additive margins across the
  settings swept, and the maximum-duration clock starts at the first echo that is
  actually served;
- a rate-limited request is the only tested drop class that keeps a session
  alive, and in one documented configuration a conforming client can observe the
  difference;
- a post-deadline packet of several tested classes consumes an expired session's
  one remaining reply, including packets that are themselves dropped;
- the reply-length cap is the bind interface's MTU on the interfaces where it was
  reachable, and a length the host cannot transmit ends the process;
- DSCP marking follows the session, with unmarked open replies and no
  cross-session leakage across interleaved sessions;
- repeated tags are last-valid-wins, with invalid-duplicate handling scoped to
  the orders actually tested, and unknown tag values are consumed as varints.

Three captures were admitted, each rewritten to the container convention already
used here and verified byte-identical in packet data and timestamps:
server-clock-absent (3 packets), server-expiry-consumption (14) and
server-dscp-interleaved (32). Two further captures were reviewed and rejected as
redundant or unsupporting; their textual results were kept instead.

Host-specific figures are labelled as such throughout, upstream defects are not
promoted to protocol requirements, observations are separated from inferences and
from recommendations, and no implementation policy is chosen here. The client
specification gains matching refinements where the two compatibility baselines
would otherwise state a lifecycle or marking rule differently.

Documentation and evidence only; no production code changed.
This commit is contained in:
Nils Andreas Svee committed 2026-08-12 16:24:57 +07:00
1 parent 035fb5a385
commit 58472dbf7b
8 files changed
+1715 -102

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
what to do instead. They are stated as observed outcomes plus **robustness
recommendations for the clean implementation** — explicitly not as
interoperability requirements, since no conforming client can observe which
choice a server made — with no description of the internal cause.
interoperability requirements. A conforming client cannot trigger the
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
@@ -463,15 +464,17 @@ and removed.
### 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;
upstream policy or default; robustness recommendation for the clean
implementation; and source-assisted historical conclusion.
black-box inference; upstream policy or default; robustness recommendation for
the clean implementation; and source-assisted historical conclusion.
Specific reclassifications made in this pass:
- Surviving a failed reply send is a **robustness recommendation**, not a
protocol MUST. No conforming client can observe which choice a server made.
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
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
@@ -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
2026-08-11: the dotted directory is the only ignored entry under `docs/`, and no
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
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 |
|------|--------|
| 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 |
| Unknown token | Dropped |
| Zero token | Dropped |
| Token of a closed session | Dropped |
| **Close** bearing a valid token from a foreign source port | **Dropped; session not closed** |
| **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
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
Resolves client-spec open question 19.3. A server with a maximum test duration
answers the first echo request past its hard deadline with a complete echo reply
carrying flags `0x06` (Reply|Close), and removes the session at that moment.
answers the first otherwise-serviceable echo request past its hard deadline with
a complete echo reply carrying flags `0x06` (Reply|Close), and removes the
session at that moment.
| Configured maximum duration | Close flag observed at |
|---|---|
@@ -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 |
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.
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
With a 2-second idle timeout, the boundary was measured between 6.9 s and 7.1 s,
i.e. timeout + 5 s. The first request past the deadline is still answered and
the session is released while handling it; only the next request is dropped.
i.e. timeout + 5 s. A post-deadline echo that would otherwise be served is
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
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,
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
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
that project, to be made and implemented separately. It is not a protocol
fact and this specification does not prescribe it.
- **Protocol:** The server echoes packets with the negotiated DSCP value.
The server MAY disallow DSCP and reset it to 0.
- **Protocol:** For negotiated values in `0..=255`, the server echoes packets
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
reply packets. Open and close packets use TOS=0 regardless of the
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
@@ -1518,12 +1527,29 @@ maximum default is 76 bytes (both+both+both+HMAC).
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
**Result:** Yes. A server that enforces a maximum test duration answers the
first echo request arriving after its hard deadline (the configured maximum
duration plus a 2-second grace, measured from the session's first echo request)
with a complete, ordinary echo reply that additionally has the Close flag set,
and removes the session at the same moment. Measured: a 1-second maximum
produced a close-flagged reply 3.01 s after the first echo; a 2-second maximum
produced one at 4.06 s.
first otherwise-serviceable echo request after its hard deadline with a complete,
ordinary echo reply that
additionally has the Close flag set, and removes the session at the same moment.
The deadline is the configured maximum duration plus a 2-second grace, measured
from the session's first echo request that is actually **served**. Measured: a
1-second maximum produced a close-flagged reply 3.01 s after the first echo; a
2-second maximum produced one at 4.06 s.
*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
negotiated duration. The mechanism exists to bound sessions from clients that
+620 -70
View File
@@ -1,7 +1,7 @@
# IRTT-Compatible Server Protocol Specification
**Version:** 1.1-verified
**Date:** 2026-08-11
**Version:** 1.2-verified
**Date:** 2026-08-12
**Compatibility baseline:** IRTT 0.9.1, protocol version 1
**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
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.
| Marker | Meaning |
|--------|---------|
| **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. |
| **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. |
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
implementation — an unbounded resource policy, a fatal reaction to a single
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.
**Discard conditions.** Each of the following was independently observed to
cause the datagram to be **silently discarded**, with no reply of any kind and
no change to the state of any live session:
cause the datagram to be **silently discarded**, with no reply of any kind:
| 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 |
| 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 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 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 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
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
@@ -331,9 +344,12 @@ A server parses the whole parameter payload. Observed acceptance rules:
| Duration present and ≤ 0 | **Dropped, no reply** |
| Interval present and ≤ 0 | **Dropped, no reply** |
| Duration or Interval absent (implicitly 0) | Accepted |
| ReceivedStats outside 0–3 | **Dropped, no reply** |
| StampAt outside 0–4 | **Dropped, no reply** |
| Clock outside 1–3 | **Dropped, no reply** |
| ReceivedStats outside 0–3, or negative | **Dropped, no reply** |
| StampAt outside 0–4, or negative | **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) |
| DSCP any value, including negative or > 255 | Accepted (see Section 7.3) |
| ServerFill string longer than 32 bytes | **Dropped, no reply** |
@@ -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
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
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
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
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
@@ -491,12 +591,12 @@ Observed outcomes:
| Case | Result |
|------|--------|
| 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) |
| Unknown token | **Dropped, no reply** |
| Zero token | **Dropped, no reply** (treated as any other unknown token) |
| 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
echo path. A capture of these cases is in
@@ -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
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
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
**Server policy.** The reference server enforces the negotiated minimum send
interval with a token-bucket allowance:
**Server policy.** The tested upstream rate limiter replenishes reply 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);
- 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
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
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
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
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
@@ -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
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
@@ -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.
**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
start of the fill pattern, and MUST NOT use payload bytes for any protocol
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
**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 |
| Repeated close with the same token | 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 |
**Protocol requirement.** A server MUST accept the close request and release the
@@ -1294,10 +1521,16 @@ duration hard limit:
- when a maximum test duration is configured, the server clamps the negotiated
duration to it during negotiation;
- it additionally enforces a hard deadline of **maximum duration + 2 seconds**,
measured from the **first echo request** of the session;
- the first echo request arriving after that deadline is answered with a normal,
complete echo reply — correct sequence number, token, statistics and
timestamps — with the Close flag additionally set;
measured from the session's **first echo request that is actually served**;
- the first echo request arriving after that deadline **that would otherwise be
served** is answered with a normal, complete echo reply — correct sequence
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
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
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
negotiated duration. The mechanism exists to bound sessions from clients that
ignore the negotiated duration.
@@ -1356,6 +1626,7 @@ token**, and the fully restricted parameter payload.
| Subsequent close with the zero token | Dropped, no reply |
| With authentication | Reply is authenticated normally; flags `0x0F` |
| 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
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
Compact reference table. "Session effect: none" means the referenced session (if
any) is left fully usable.
Compact reference table. For rows whose session state is **live**, "Session
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 |
|---|---|---|---|---|
@@ -1399,8 +1672,8 @@ any) is left fully usable.
| 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 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 |
| Echo after idle expiry deadline | expired | ok | **reply** (served once) | 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) |
| 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 |
| 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 |
@@ -1439,7 +1712,7 @@ default **1 minute**, with `0` meaning "never expire".
**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 |
| 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:
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
2-second timeout.
2. **The first request that arrives after the deadline is still answered**, and
the session is torn down as part of handling it. Only the *next* request is
dropped. A compatible implementation MAY drop the first post-deadline request
instead; clients cannot depend on either behavior, because from their side
the deadline coincides with an unresponsive server.
2. **In observed upstream behavior, the first otherwise-serviceable echo that
reaches an expired session receives a final reply.** The immediate follow-up
is then dropped, which is how release is observed externally.
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
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
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.
### 18.3 Resource Limits
@@ -1538,9 +1894,9 @@ implementation.
| Default minimum send interval | 10 ms | policy |
| Default burst allowance | 5 requests | 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-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 packet length | unlimited | policy |
| Default fill pattern | repeating `69 72 74 74` | policy |
@@ -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
(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
directly as the TOS / Traffic Class byte of its echo replies, and a compatible
implementation MUST encode and interpret it the same way. The common Expedited
Forwarding marking is therefore **0xb8 (184)** on the wire, not 46. Verified by
capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
**0–255**. It is not a 6-bit codepoint. For negotiated values in that range, a
server applies the value directly as the TOS / Traffic Class byte of its echo
replies, and a compatible implementation MUST encode and interpret it the same
way. The common Expedited Forwarding marking is therefore **0xb8 (184)** on the
wire, not 46. Verified by capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
`captures/dscp-session.pcapng`.
> **Out of scope: user-facing DSCP notation.** Whether an implementation's
@@ -1573,28 +1929,108 @@ capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
> upstream wire behavior, and it does not belong in this specification.
**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
negotiated value is forced to 0 and returned as absent from the parameter
applies an in-range negotiated value to its echo replies. When DSCP is disallowed,
the negotiated value is forced to 0 and returned as absent from the parameter
payload.
**Observed range handling.** The negotiated value is **not** range-checked
during negotiation. Values 0–255 are applied normally. A value above 255 is
accepted and echoed back in the open reply, but then every echo reply for that
session fails to send and is dropped — the client sees a session that opens and
then answers nothing.
**Observed range handling on the tested host.** Values in `0..=255` were applied
normally. A value of 256 or above was accepted and echoed back in the open reply,
but no echo replies were observed for that session. Other sessions continued to
be served normally throughout.
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
values outside 0–255 during negotiation rather than accepting a value that cannot
be applied, so that the value returned in the open reply is one the server will
honour.
values outside `0..=255` during negotiation rather than accepting a negotiated
value that leads to an observed no-reply session, so that the value returned in
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
the client specification, Section 10.8).
**What "close packets are unmarked" does and does not mean.** The client
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
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
Two independent things are recorded here and must not be conflated.
@@ -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.
Everything in this section is an **observation plus a robustness
recommendation**. None of it is an interoperability requirement: no conforming
client can tell which choice a server made, because in every case the visible
effect is either silence or an ordinary reply.
recommendation**. None of it is an interoperability requirement, in the specific
sense that no conforming client can *provoke* any of these outcomes: reaching
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
@@ -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
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
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
this is reachable without any malformed 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
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
@@ -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
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
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.
**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
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
@@ -1807,12 +2340,20 @@ inspected, what was attempted, and what evidence would settle it.
does it vary with how the platform reports interface MTUs?
- **Why it matters:** It determines the largest length a server can honestly
negotiate.
- **Evidence inspected:** Measured on one host: replies capped at exactly the
bind interface's MTU (1500 on an Ethernet interface); loopback with a
16384-byte MTU was masked by a smaller maximum outbound datagram size.
- **Attempted:** Two interfaces with different MTUs on one host.
- **Evidence inspected:** Measured on one host across three interfaces —
loopback (16384), Ethernet (1500) and a tunnel (1280). On the **Ethernet and
tunnel** interfaces the reply was capped at exactly that interface's MTU, byte
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
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
@@ -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
session for it.
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.
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
@@ -1898,21 +2441,28 @@ in Section 24.3 and are deliberately not mixed in.
### 24.3 Robustness recommendations for the clean implementation
Not interoperability requirements. No conforming client can observe whether these
were followed; they are here because each addresses a behavior observed upstream
that a new implementation should not copy.
Not standalone interoperability requirements. They are here because each
addresses a behavior observed upstream that a new implementation should not
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
(Sections 6.3, 22.1).
1. Treat a per-packet send failure as a per-packet event, not a fatal one, and
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
never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits
and eviction policy are a clean-project decision.
3. Validate a fill descriptor before use, and reject a zero-length pattern
(Section 22.2).
4. Reject or clamp DSCP values the socket cannot carry, during negotiation
(Section 20).
4. Reject or clamp DSCP values outside `0..=255` during negotiation to avoid
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
(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
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`
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.
Unless a row says otherwise, the server under test is the unmodified upstream
0.9.1 release. Two other builds appear only in Section 5.2.3, each named
explicitly: the 0.9.0 release, and a build of the upstream development tree six
upstream commits past the 0.9.1 tag. Results from those builds never stand in for
0.9.1.
0.9.1 release. Two other builds appear where they are named explicitly: the 0.9.0
release, and a build of the upstream development tree six upstream commits past
the 0.9.1 tag. Results from those builds never stand in for 0.9.1. Section 5.2.3
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
server-assigned random values and differ per run; where a token appears below it
@@ -264,6 +266,38 @@ session:
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
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
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
Requested StampAt → negotiated StampAt, by server timestamp allowance:
@@ -372,6 +420,68 @@ Requested StampAt → negotiated StampAt, by server timestamp allowance:
| 3 (both) | 3 | **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
@@ -709,8 +819,10 @@ start the idle clock, wait, then two echoes back to back.
| 7.5 s | answered | **dropped** |
| 9.0 s | answered | **dropped** |
Deadline = idle timeout + 5 s. The first request past the deadline is still
answered, and the session is released while handling it.
Observed session-release boundary = idle timeout + 5 s. The first
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
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
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
@@ -768,15 +972,74 @@ therefore 184 (0xb8) in this table, not 46.
| 46 | 46 | delivered |
| 184 | 184 | delivered |
| 255 | 255 | delivered |
| 256 | 256 | **all dropped** |
| 300 | 300 | **all dropped** |
| 256 | 256 | **no ECHO reply observed** |
| 300 | 300 | **no ECHO reply observed** |
| −5 | −5 | delivered |
With DSCP disallowed by server policy, every requested value is returned as
absent (0) and replies are delivered normally.
A value the server cannot apply to its socket is still accepted during
negotiation; the session then opens successfully and answers nothing.
An out-of-range value is still accepted during negotiation; for values of 256 or
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.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-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
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**
altered: for each file, the full frame hexdump and the per-frame epoch timestamps
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.