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:
8 files changed
+1715
-102
No files matched your search
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
Binary file not shown.
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.
|
||||
Reference in new issue
Block a user