Merge pull request #76 from Lochnair/docs/server-deep-dive-airlock
docs(protocol): airlock server black-box findings
This commit was merged in pull request #76.
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
|
reachable from ordinary client traffic and a clean implementation has to decide
|
||||||
what to do instead. They are stated as observed outcomes plus **robustness
|
what to do instead. They are stated as observed outcomes plus **robustness
|
||||||
recommendations for the clean implementation** — explicitly not as
|
recommendations for the clean implementation** — explicitly not as
|
||||||
interoperability requirements, since no conforming client can observe which
|
interoperability requirements. A conforming client cannot trigger the
|
||||||
choice a server made — with no description of the internal cause.
|
absent-Clock defect through conforming negotiation, but can observe a
|
||||||
|
process-wide outage if another peer triggers it; no internal cause is described.
|
||||||
|
|
||||||
### Server-Pass Sanitization Audit
|
### Server-Pass Sanitization Audit
|
||||||
|
|
||||||
@@ -463,15 +464,17 @@ and removed.
|
|||||||
|
|
||||||
### Statement classification
|
### Statement classification
|
||||||
|
|
||||||
The outgoing documents now separate five kinds of statement and never conflate
|
The outgoing documents now separate six kinds of statement and never conflate
|
||||||
them: interoperability requirement; black-box observed upstream behavior;
|
them: interoperability requirement; black-box observed upstream behavior;
|
||||||
upstream policy or default; robustness recommendation for the clean
|
black-box inference; upstream policy or default; robustness recommendation for
|
||||||
implementation; and source-assisted historical conclusion.
|
the clean implementation; and source-assisted historical conclusion.
|
||||||
|
|
||||||
Specific reclassifications made in this pass:
|
Specific reclassifications made in this pass:
|
||||||
|
|
||||||
- Surviving a failed reply send is a **robustness recommendation**, not a
|
- Surviving a failed reply send is a **robustness recommendation**, not a
|
||||||
protocol MUST. No conforming client can observe which choice a server made.
|
protocol MUST. A process-wide failure can be visible to unrelated conforming
|
||||||
|
clients, but that does not make a particular clean resilience policy an
|
||||||
|
interoperability requirement.
|
||||||
- Upstream's absence of session and per-peer bounds is **observed policy**, and
|
- Upstream's absence of session and per-peer bounds is **observed policy**, and
|
||||||
the documents now say explicitly that it is not something a compatible server
|
the documents now say explicitly that it is not something a compatible server
|
||||||
should reproduce. The clean server is expected to be bounded; the shape of that
|
should reproduce. The clean server is expected to be bounded; the shape of that
|
||||||
@@ -610,3 +613,243 @@ macOS Finder metadata file previously listed here is no longer present; the
|
|||||||
exclusion still stands, since Finder can recreate one at any time. Re-verified
|
exclusion still stands, since Finder can recreate one at any time. Re-verified
|
||||||
2026-08-11: the dotted directory is the only ignored entry under `docs/`, and no
|
2026-08-11: the dotted directory is the only ignored entry under `docs/`, and no
|
||||||
Finder metadata file exists anywhere in the outgoing tree.
|
Finder metadata file exists anywhere in the outgoing tree.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Deep-Dive Airlock (2026-08-12)
|
||||||
|
|
||||||
|
This section records **provenance and boundary discipline** for the material added
|
||||||
|
on 2026-08-12. It is not implementation archaeology, and it deliberately contains
|
||||||
|
no detail about what the contaminated-side work looked at.
|
||||||
|
|
||||||
|
### What happened outside this repository
|
||||||
|
|
||||||
|
A further source-assisted research phase on the upstream server took place in a
|
||||||
|
separate contaminated workspace, outside this repository and outside `docs/`. As
|
||||||
|
in the earlier passes, that workspace held upstream source, upstream tests,
|
||||||
|
upstream history, source-assisted analysis, purpose-built harnesses, build trees,
|
||||||
|
binaries, raw result files, server logs, and black-box packet evidence. The
|
||||||
|
overwhelming majority of it is not admissible here and did not cross.
|
||||||
|
|
||||||
|
### What was admitted, and what the airlock agent was allowed to see
|
||||||
|
|
||||||
|
The transfer was performed by an agent working in this clean repository under an
|
||||||
|
explicit airlock restriction. Its **entire** permitted view of the contaminated
|
||||||
|
workspace was:
|
||||||
|
|
||||||
|
1. a single sanitizer-candidate report of **independently black-box-verified**
|
||||||
|
findings — written to be transferable without upstream source access, and
|
||||||
|
containing no source snippets, filenames, function or type names, or internal
|
||||||
|
structure; and
|
||||||
|
2. the black-box packet captures that report referenced, for the purposes of
|
||||||
|
confirming the packet-level evidence, checking container metadata, and copying
|
||||||
|
suitable files into the clean evidence set.
|
||||||
|
|
||||||
|
The agent was explicitly **prohibited** from inspecting, and did not inspect:
|
||||||
|
|
||||||
|
- the source-assisted findings report;
|
||||||
|
- the unresolved/source question matrix;
|
||||||
|
- the contaminated workspace's own README;
|
||||||
|
- upstream source, upstream tests, or upstream git history;
|
||||||
|
- any upstream checkout;
|
||||||
|
- the research build trees and harnesses;
|
||||||
|
- raw result files;
|
||||||
|
- server logs.
|
||||||
|
|
||||||
|
It was further prohibited from searching the contaminated workspace at large. No
|
||||||
|
recursive search, and no history or blame inspection, was performed there; the
|
||||||
|
only accesses were direct reads of the two authorized items above. No raw result
|
||||||
|
file was opened at all — the authorized report's own text proved sufficient for
|
||||||
|
every statement transferred, so the narrowest available boundary was kept.
|
||||||
|
|
||||||
|
### What the boundary cost, deliberately
|
||||||
|
|
||||||
|
Several items in the authorized report were **not** transferred, and the reasons
|
||||||
|
are worth recording because they are the boundary doing its job:
|
||||||
|
|
||||||
|
- An account of the order in which the server reports rejection reasons, taken
|
||||||
|
from the server's own diagnostic output. Even though that output is externally
|
||||||
|
emitted, it describes internal check ordering, and the clean specification had
|
||||||
|
already deliberately replaced its admission narrative with a table of discard
|
||||||
|
conditions plus the two precedence relationships that were verified on the
|
||||||
|
wire. Re-importing an ordering narrative would have undone that work.
|
||||||
|
- A statement that a session is removed before its close-flagged reply is sent.
|
||||||
|
That is internal ordering; only its externally visible consequence crossed, and
|
||||||
|
that consequence was already recorded.
|
||||||
|
- A generalisation about which flag combinations are dropped on a data packet
|
||||||
|
carrying a live token. The clean documents already carry a **more precise**
|
||||||
|
account of the same behavior, and the broader statement would have weakened it.
|
||||||
|
- An inference that the wire format offers no general extension channel. It
|
||||||
|
crossed only as an explicitly labelled inference, alongside the byte encodings
|
||||||
|
actually tested, rather than as a property of the format.
|
||||||
|
|
||||||
|
Where the authorized report was insufficient to support a clean statement, the
|
||||||
|
statement was not made and the ambiguity was left standing. Nothing was resolved
|
||||||
|
by consulting anything else.
|
||||||
|
|
||||||
|
### Classification discipline
|
||||||
|
|
||||||
|
Every new statement was classified before it was written, and the classification
|
||||||
|
appears in the text: observed upstream behavior, black-box inference, upstream
|
||||||
|
policy, protocol requirement, robustness recommendation for the clean
|
||||||
|
implementation, or remaining unknown. The server specification's statement-level
|
||||||
|
table was extended with the inference level and with the host-specific marking
|
||||||
|
that several of these results require.
|
||||||
|
|
||||||
|
Two boundaries were held particularly deliberately:
|
||||||
|
|
||||||
|
- **An upstream defect was not promoted to a protocol requirement.** The most
|
||||||
|
significant finding of this pass is an open request that upstream accepts and
|
||||||
|
then faults on. It is recorded as an observed robustness defect with a
|
||||||
|
must-not-crash recommendation for the clean server, and the documents state
|
||||||
|
explicitly that reproducing the fault is not an interoperability requirement.
|
||||||
|
- **No implementation policy was chosen.** The documents record what upstream
|
||||||
|
does about an open that selects timestamps without a clock, about a negative
|
||||||
|
Length, about out-of-range DSCP values, and about payload fill — and in
|
||||||
|
each case say that the clean project's choice is a decision for the
|
||||||
|
implementation work, not one this evidence settles. Those decisions remain open.
|
||||||
|
|
||||||
|
Host-specific figures — interface MTUs, the tested host's maximum outbound
|
||||||
|
datagram size, interface-MTU effective-length decision knees, and platform
|
||||||
|
handling of out-of-range DSCP values — are labelled as host-specific wherever
|
||||||
|
they appear, so that none is mistaken for a protocol constant or compiled into an
|
||||||
|
implementation.
|
||||||
|
|
||||||
|
### Captures admitted
|
||||||
|
|
||||||
|
Three captures crossed, each because it materially supports a finding that is new
|
||||||
|
to the clean documents:
|
||||||
|
|
||||||
|
| File | Packets | What it proves |
|
||||||
|
|------|---------|----------------|
|
||||||
|
| `../protocol/captures/server-clock-absent.pcapng` | 3 | An open selecting timestamps with no Clock tag is answered with a token and no Clock parameter, and the first echo is the last packet in the exchange |
|
||||||
|
| `../protocol/captures/server-expiry-consumption.pcapng` | 14 | Control and foreign-endpoint-first test: whether the first tested packet to reach an expired session emits a reply |
|
||||||
|
| `../protocol/captures/server-dscp-interleaved.pcapng` | 32 | Four interleaved sessions with distinct DSCP values over three rounds, no cross-session leakage, unmarked open replies |
|
||||||
|
|
||||||
|
Each was reviewed before copying and rewritten into the container convention
|
||||||
|
already used here: packet records and the rewriting tool's own version string
|
||||||
|
only, with no capture comment, no section or interface description, no host, user
|
||||||
|
or machine identification, no capture filter, and no filesystem path. Packet
|
||||||
|
bytes and per-frame timestamps were **not** altered — for each file the full
|
||||||
|
frame hexdump and the per-frame epoch timestamps were verified identical before
|
||||||
|
and after, and the packet counts above are unchanged from the originals. The
|
||||||
|
copied files were re-inspected afterwards and contain no string other than the
|
||||||
|
rewriting tool's version.
|
||||||
|
|
||||||
|
Two further captures were reviewed and **not** admitted: one materially redundant
|
||||||
|
with a capture already present, and one that on inspection did not demonstrate
|
||||||
|
the behavior it was associated with. In both cases the textual result was kept
|
||||||
|
instead, which is the preferred trade — a textual black-box result is better than
|
||||||
|
an unnecessary addition to the evidence set. The reasoning is recorded in the
|
||||||
|
verification report rather than only here.
|
||||||
|
|
||||||
|
### Review corrections applied before merge
|
||||||
|
|
||||||
|
Review of the airlock commit found several places where the wording claimed more
|
||||||
|
than the admitted evidence supports. Each was narrowed to the observation, using
|
||||||
|
the clean evidence set only — no contaminated material was consulted to resolve
|
||||||
|
any of them:
|
||||||
|
|
||||||
|
- The reply-length cap is stated as byte-exact only for the two interfaces where
|
||||||
|
that was measurable. The loopback row shows lengths up to 8000 emitted
|
||||||
|
unclamped and nothing about a 16384-byte boundary, which this host cannot reach
|
||||||
|
because a reply beyond roughly 9300 bytes ends the server first.
|
||||||
|
- The interleaved DSCP capture is described as the three rounds it contains, and
|
||||||
|
the separate observation that an open reply is unmarked on a listener that has
|
||||||
|
already sent marked replies is attributed to the capture that actually shows it
|
||||||
|
— an existing capture from the first pass — rather than to the new one, whose
|
||||||
|
open exchanges all precede its first marked reply.
|
||||||
|
- The fill-phase result is scoped to continuity across the sessions tested and
|
||||||
|
across the tested IPv4 and IPv6 listeners, rather than asserting one stream
|
||||||
|
shared by every session of a process. The recommendation previously drawn from
|
||||||
|
it was **removed**: the default fill is a fixed public pattern, so its phase
|
||||||
|
carrying across sessions discloses nothing about another peer, and the
|
||||||
|
observation does not justify prescribing per-session fill state. The genuine
|
||||||
|
no-cross-peer-data invariant was already stated for the no-fill mode and is
|
||||||
|
untouched.
|
||||||
|
- The must-not-crash advice no longer uses RFC 2119 keywords, which this document
|
||||||
|
reserves for interoperability requirements. It is a robustness recommendation,
|
||||||
|
and how strictly the implementing project binds itself to it belongs in that
|
||||||
|
project's own guidance.
|
||||||
|
- The presence-versus-zero rule is scoped to the reply direction. Requests do
|
||||||
|
carry present zeros — `Length = 0` and `DSCP = 0` are accepted, and `Clock = 0`
|
||||||
|
is rejected precisely because it is present.
|
||||||
|
- The admission-table row for an open with no Clock now says the **open** is
|
||||||
|
answered normally, not the session, which is what the evidence shows.
|
||||||
|
- The maximum-duration origin refinement was carried into the client
|
||||||
|
specification as well, since both documents are compatibility baselines and
|
||||||
|
would otherwise state the lifecycle rule differently.
|
||||||
|
|
||||||
|
A second review round found five further places where the first correction pass
|
||||||
|
had left an inconsistency behind, and these were fixed the same way:
|
||||||
|
|
||||||
|
- The must-not-crash advice is called a **recommendation** everywhere, including
|
||||||
|
in the paragraph that follows it and in the behavioral vector, so that dropping
|
||||||
|
the RFC 2119 keywords was not undone two sentences later.
|
||||||
|
- The remaining-unknown entry on reply-length capping was brought into line with
|
||||||
|
the corrected §9.2: exact only for the Ethernet and tunnel interfaces, with the
|
||||||
|
loopback trial recorded as a lower bound.
|
||||||
|
- The maximum-duration origin is stated for the **two** drop classes actually
|
||||||
|
tested in the first-request position — oversized and foreign-endpoint — instead
|
||||||
|
of generalising to any dropped request. Corrected in the specification, the
|
||||||
|
vector and the verification report together.
|
||||||
|
- The "first post-deadline request is still answered" rule is qualified to a
|
||||||
|
request that would otherwise be served, since the same subsection then shows a
|
||||||
|
foreign-endpoint packet being dropped while still consuming the release.
|
||||||
|
- "Close packets are unmarked" is scoped to standalone datagrams. A
|
||||||
|
server-initiated close is an echo reply carrying the session's marking, and
|
||||||
|
both specifications now say so rather than leaving the two statements to
|
||||||
|
contradict each other.
|
||||||
|
|
||||||
|
A final documentation-only review correction restricted the maximum-duration
|
||||||
|
dropped-trigger result to the directly tested rate-limit case and corrected
|
||||||
|
fill alternatives as observable but interoperability-equivalent. It used no new
|
||||||
|
evidence or contaminated material.
|
||||||
|
|
||||||
|
None of these corrections required new evidence, and none changed a
|
||||||
|
classification from observation to requirement or the reverse.
|
||||||
|
|
||||||
|
No binaries, source, harnesses, build trees, server logs, result files or
|
||||||
|
research directories crossed.
|
||||||
|
|
||||||
|
### Clean-Room Compliance Checklist (Deep-Dive Airlock)
|
||||||
|
|
||||||
|
- [x] Source-assisted findings report never opened.
|
||||||
|
- [x] Unresolved/source question matrix never opened.
|
||||||
|
- [x] Upstream source, tests and history never opened.
|
||||||
|
- [x] Research build trees and harnesses never opened.
|
||||||
|
- [x] Raw result files never opened.
|
||||||
|
- [x] Server logs never opened.
|
||||||
|
- [x] No recursive search, log or blame inspection performed in the contaminated
|
||||||
|
workspace.
|
||||||
|
- [x] Only independently black-box-verified evidence transferred.
|
||||||
|
- [x] Every transferred statement classified, with observation separated from
|
||||||
|
inference and from recommendation.
|
||||||
|
- [x] Upstream defects recorded as defects, not as protocol requirements.
|
||||||
|
- [x] Host-specific figures labelled as host-specific.
|
||||||
|
- [x] Clean implementation policy decisions left open for the implementation work.
|
||||||
|
- [x] Captures metadata-reviewed, container-rewritten, and verified
|
||||||
|
byte-identical in packet data and timestamps.
|
||||||
|
- [x] No production code changed by the airlock work.
|
||||||
|
|
||||||
|
### 2026-08-12 — PR #76 final review wording cleanup
|
||||||
|
|
||||||
|
An interrupted prior agent's local maximum-duration and rate-limit corrections
|
||||||
|
were reviewed rather than discarded: the maximum-duration trigger clarification
|
||||||
|
was retained, while the rate-limit interaction was kept as a black-box inference
|
||||||
|
rather than promoted to unsupported RFC 2119 language. Using clean evidence only,
|
||||||
|
this review also removed the false request-length invariant, scoped normal DSCP
|
||||||
|
marking to `0..=255`, and clarified that a dropped first post-expiry packet can
|
||||||
|
release the session before any final reply is emitted. No contaminated material
|
||||||
|
was consulted.
|
||||||
|
|
||||||
|
### 2026-08-12 — PR #76 black-box claim review correction
|
||||||
|
|
||||||
|
The latest review found a blanket discard-state claim that conflicted with the
|
||||||
|
observed rate-limit idle refresh, stale compact maximum-duration and idle-expiry
|
||||||
|
summaries, and an over-prescriptive parameter-presence statement. It also found
|
||||||
|
two black-box observations expressed as internal mechanism claims: pre-policy
|
||||||
|
receive truncation and a rejected OPEN leaving no internal state; configured and
|
||||||
|
negotiated limiter intervals had likewise been conflated. The wording now states
|
||||||
|
observable results or explicit inference only. No contaminated material was
|
||||||
|
consulted, no new evidence was added, and no implementation policy was chosen.
|
||||||
@@ -693,15 +693,18 @@ The upstream count is six.)*
|
|||||||
A post-open request is bound to a session by its token **and** by an exact match
|
A post-open request is bound to a session by its token **and** by an exact match
|
||||||
on the source address, source port, and address family.
|
on the source address, source port, and address family.
|
||||||
|
|
||||||
|
The identity trials below used live, non-expired sessions. Finding S-21 records
|
||||||
|
the separately observed expired-session release case.
|
||||||
|
|
||||||
| Case | Result |
|
| Case | Result |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Correct token, original endpoint | Served |
|
| Correct token, original endpoint | Served |
|
||||||
| Correct token, different source port | **Dropped, no reply; session intact** |
|
| Correct token, different source port | **Dropped, no reply; the live session remained usable from its original endpoint** |
|
||||||
| Correct token, other address family's listener on the same port | Dropped |
|
| Correct token, other address family's listener on the same port | Dropped |
|
||||||
| Unknown token | Dropped |
|
| Unknown token | Dropped |
|
||||||
| Zero token | Dropped |
|
| Zero token | Dropped |
|
||||||
| Token of a closed session | Dropped |
|
| Token of a closed session | Dropped |
|
||||||
| **Close** bearing a valid token from a foreign source port | **Dropped; session not closed** |
|
| **Close** bearing a valid token from a foreign source port | **Dropped; does not close an otherwise live session** |
|
||||||
|
|
||||||
Endpoint binding protects the close path as well as the echo path. A capture is
|
Endpoint binding protects the close path as well as the echo path. A capture is
|
||||||
in `captures/server-session-identity.pcapng`.
|
in `captures/server-session-identity.pcapng`.
|
||||||
@@ -751,8 +754,9 @@ produced 5 replies with counts 1–5, and three later requests continued 6–8.
|
|||||||
## Finding S-4: Server-initiated close exists and is triggered by the duration limit
|
## Finding S-4: Server-initiated close exists and is triggered by the duration limit
|
||||||
|
|
||||||
Resolves client-spec open question 19.3. A server with a maximum test duration
|
Resolves client-spec open question 19.3. A server with a maximum test duration
|
||||||
answers the first echo request past its hard deadline with a complete echo reply
|
answers the first otherwise-serviceable echo request past its hard deadline with
|
||||||
carrying flags `0x06` (Reply|Close), and removes the session at that moment.
|
a complete echo reply carrying flags `0x06` (Reply|Close), and removes the
|
||||||
|
session at that moment.
|
||||||
|
|
||||||
| Configured maximum duration | Close flag observed at |
|
| Configured maximum duration | Close flag observed at |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -760,7 +764,8 @@ carrying flags `0x06` (Reply|Close), and removes the session at that moment.
|
|||||||
| 2 s | 4.06 s / 4.10 s after the first echo |
|
| 2 s | 4.06 s / 4.10 s after the first echo |
|
||||||
|
|
||||||
The deadline is the maximum duration plus a 2-second grace, measured from the
|
The deadline is the maximum duration plus a 2-second grace, measured from the
|
||||||
session's first echo request — not from open. Capture:
|
session's first **served** echo — not from open. A deadline-crossing echo that is
|
||||||
|
itself rate-limited is dropped; the next served echo carries Close. Capture:
|
||||||
`captures/server-close.pcapng`, frame 36.
|
`captures/server-close.pcapng`, frame 36.
|
||||||
|
|
||||||
Driving an upstream 0.9.1 client against a server that sets the flag mid-test:
|
Driving an upstream 0.9.1 client against a server that sets the flag mid-test:
|
||||||
@@ -871,8 +876,9 @@ consecutive failing probes and served the next valid request normally.
|
|||||||
## Finding S-11: Idle expiry has a 5-second grace, and never-used sessions do not expire
|
## Finding S-11: Idle expiry has a 5-second grace, and never-used sessions do not expire
|
||||||
|
|
||||||
With a 2-second idle timeout, the boundary was measured between 6.9 s and 7.1 s,
|
With a 2-second idle timeout, the boundary was measured between 6.9 s and 7.1 s,
|
||||||
i.e. timeout + 5 s. The first request past the deadline is still answered and
|
i.e. timeout + 5 s. A post-deadline echo that would otherwise be served is
|
||||||
the session is released while handling it; only the next request is dropped.
|
answered and releases the session while handling it; only the next request is
|
||||||
|
dropped.
|
||||||
|
|
||||||
A session that has never carried an echo request does not expire at all: opened
|
A session that has never carried an echo request does not expire at all: opened
|
||||||
and left idle for 9 seconds under a 2-second timeout, it was served normally.
|
and left idle for 9 seconds under a 2-second timeout, it was served normally.
|
||||||
@@ -1181,3 +1187,521 @@ disregards it loses nothing normative.
|
|||||||
**Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1,
|
**Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1,
|
||||||
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
|
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
|
||||||
19.14).
|
19.14).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Part IV — Server Deep-Dive Black-Box Pass (2026-08-12)
|
||||||
|
|
||||||
|
A further black-box pass over the server, aimed at behavior that only a
|
||||||
|
non-conforming peer can reach: parameter presence, the lifetime boundaries, the
|
||||||
|
drop classes, reply sizing, and IP marking. It resolves several statements that
|
||||||
|
earlier parts left thin, and it records one previously undocumented upstream
|
||||||
|
robustness defect that is reachable from two unauthenticated datagrams.
|
||||||
|
|
||||||
|
## Part IV Method and Environment
|
||||||
|
|
||||||
|
**Builds exercised**, all unmodified, all run as ordinary server processes with
|
||||||
|
documented command-line options only:
|
||||||
|
|
||||||
|
| Build | Role in this part |
|
||||||
|
|-------|-------------------|
|
||||||
|
| **0.9.1 release** | the baseline; every unqualified statement refers to it |
|
||||||
|
| **0.9.0 release** | reproduction check |
|
||||||
|
| **development tree, six upstream commits past the 0.9.1 tag** | reproduction check; post-release work, not part of any release |
|
||||||
|
|
||||||
|
**Platform:** macOS Darwin 25.5.0 arm64. Loopback except where an Ethernet or
|
||||||
|
tunnel interface is named.
|
||||||
|
|
||||||
|
**Instrument.** An independent raw-UDP harness sharing no code with any IRTT
|
||||||
|
implementation. It performs the open negotiation itself, emits exact request
|
||||||
|
bytes, and records exact reply bytes and monotonic arrival times. For IP marking
|
||||||
|
it reads the received TOS / traffic-class byte from the kernel.
|
||||||
|
|
||||||
|
**Server provenance.** Each server under test is an unmodified build of the
|
||||||
|
revision named in the table above — the 0.9.0 release, the 0.9.1 release, or the
|
||||||
|
development tree six upstream commits past the 0.9.1 tag. No source change,
|
||||||
|
patch, instrumentation or test hook was applied to any of them, and each was run
|
||||||
|
as an ordinary server process configured only through documented command-line
|
||||||
|
options.
|
||||||
|
|
||||||
|
**What counts as evidence here.** Request bytes, reply bytes, the presence or
|
||||||
|
absence of a reply, the reply's IP header marking, arrival times, and whether a
|
||||||
|
server process was still serving afterwards. Timing boundaries were established
|
||||||
|
by sweeping probe offsets, not by reading any value out of anything. Where a
|
||||||
|
result depends on the tested host — interface MTUs, the maximum outbound datagram
|
||||||
|
size, socket handling of out-of-range values — it is labelled host-specific below
|
||||||
|
and in the specification.
|
||||||
|
|
||||||
|
**Repeatability.** Timing boundaries were swept at 50 ms spacing with three
|
||||||
|
repetitions per configured value. Behavioral results are single-shot unless a
|
||||||
|
repetition count is given.
|
||||||
|
|
||||||
|
## Finding S-19: An open whose restricted StampAt remains non-none without a Clock is accepted, then fatal
|
||||||
|
|
||||||
|
The most serious result of this pass.
|
||||||
|
|
||||||
|
When timestamp restriction leaves the restricted (effective) `StampAt` non-none,
|
||||||
|
an open request carrying `StampAt` = 1, 2, 3 or 4 and **no Clock tag at all** is
|
||||||
|
accepted: the server returns an ordinary session-creating reply with a valid
|
||||||
|
non-zero token, and the reply carries no Clock parameter — none is synthesized.
|
||||||
|
The **first** echo request on that session receives no reply and the server
|
||||||
|
process terminates. On a wildcard bind, the listener for the other address family
|
||||||
|
dies with it, so unrelated sessions stop being served at the same moment.
|
||||||
|
|
||||||
|
```
|
||||||
|
C -> S 14a75b01 05 06 06 06 04 00
|
||||||
|
open; ReceivedStats=3, StampAt=3 (both), Length=0, no Clock tag
|
||||||
|
S -> C 14a75b03 7ca44127b2cd7eab 01 02 05 06 06 06
|
||||||
|
open reply; non-zero token, ProtocolVersion 1, ReceivedStats 3,
|
||||||
|
StampAt 3 — no Clock parameter
|
||||||
|
C -> S 14a75b00 7ca44127b2cd7eab 00000000
|
||||||
|
first echo, sequence 0 -> no reply; server no longer serving
|
||||||
|
```
|
||||||
|
|
||||||
|
| Input | Outcome |
|
||||||
|
|-------|---------|
|
||||||
|
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; restricted StampAt remains non-none | open accepted, first echo fatal |
|
||||||
|
| Requested StampAt 0, Clock tag absent | open accepted, serves normally, no timestamps |
|
||||||
|
| StampAt 3, **explicit `Clock = 0`** | **open dropped** — no reply, no session |
|
||||||
|
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; no-timestamp policy restricts StampAt to none | open accepted, serves normally, no timestamps |
|
||||||
|
| Requested StampAt 1 / 2 / 3 / 4, Clock tag absent; single-timestamp policy leaves restricted StampAt non-none | open accepted, first echo still fatal |
|
||||||
|
|
||||||
|
Reproduced for all four non-none StampAt values on **0.9.0, 0.9.1 and the
|
||||||
|
development build**. Capture: `captures/server-clock-absent.pcapng` (3 packets).
|
||||||
|
|
||||||
|
The absent-versus-explicit-zero contrast is the load-bearing part: the two inputs
|
||||||
|
differ by one encoded tag and produce opposite outcomes, so parameter *presence*
|
||||||
|
is externally meaningful in this upstream behavior. An implementation that
|
||||||
|
chooses to reproduce those distinct outcomes needs request validation that can
|
||||||
|
distinguish the two forms; this observation does not prescribe a clean server's
|
||||||
|
representation or policy for adversarial inputs.
|
||||||
|
|
||||||
|
Nothing here describes why the server stops. That is not observable from the
|
||||||
|
wire, it is not stated, and no conclusion in this report depends on it.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION — upstream robustness defect
|
||||||
|
(server spec Sections 7.2, 22.5, 24.3; vectors Section 4.7).
|
||||||
|
|
||||||
|
## Finding S-20: A rate-limited request is the only tested drop class that keeps a session alive
|
||||||
|
|
||||||
|
For each drop class: open a session, send one echo, inject the packet under test,
|
||||||
|
send another echo, and read the second reply's count and window. Separate timing
|
||||||
|
trials established whether the injected packet moved the expiry boundary.
|
||||||
|
|
||||||
|
Every class tested advanced neither the received count nor the received window.
|
||||||
|
They divide on the second question:
|
||||||
|
|
||||||
|
| Injected packet | Extends the observed idle lifetime |
|
||||||
|
|-----------------|------------------------------------|
|
||||||
|
| Bad magic, invalid flag bits, Reply/Open flag on a data packet | no |
|
||||||
|
| Unknown token, zero token | no |
|
||||||
|
| Valid token from a foreign source endpoint | no (but see S-21) |
|
||||||
|
| Truncated echo, oversized echo | no |
|
||||||
|
| Bad, missing, zeroed or truncated MAC | no |
|
||||||
|
| **Request arriving with no rate allowance left** | **yes** |
|
||||||
|
|
||||||
|
The rate-limit result was separated from its alternatives three ways rather than
|
||||||
|
asserted from one trial: a session fed *only* too-fast requests outlived its idle
|
||||||
|
deadline; a session fed the same cadence of oversized requests expired on
|
||||||
|
schedule; and a silent session expired on schedule. A two-probe method was used
|
||||||
|
at the boundary so that "still alive" is distinguished from "expired but not yet
|
||||||
|
touched" — the distinction S-21 turns on. Reproduced on all three builds.
|
||||||
|
|
||||||
|
This is a statement about how long the session kept being served. What a server
|
||||||
|
records when it declines a request is not observable and is not claimed.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens the previously
|
||||||
|
unelaborated statement in server spec Section 18.2 (now Sections 9.3, 18.2;
|
||||||
|
vectors Section 8.2).
|
||||||
|
|
||||||
|
## Finding S-21: The first packet to release an expired session determines whether a final reply is emitted
|
||||||
|
|
||||||
|
Part II established that a post-deadline echo that would otherwise be served is
|
||||||
|
answered and releases the session while handling it. This pass establishes that
|
||||||
|
a packet which would normally be dropped can instead release the session without
|
||||||
|
receiving a reply, so no later request receives a final reply.
|
||||||
|
|
||||||
|
Both trials are in one capture, `captures/server-expiry-consumption.pcapng`
|
||||||
|
(14 packets), with a 3-second idle timeout — an 8-second effective boundary:
|
||||||
|
|
||||||
|
| Trial | Sequence | Client's first post-deadline echo |
|
||||||
|
|-------|----------|-----------------------------------|
|
||||||
|
| Control | nothing between the deadline and the client's request | **answered** (count 2); the next one dropped |
|
||||||
|
| Test | an echo bearing the correct token from a **foreign source port** arrived 0.3 s earlier, just past the same deadline | **no reply**, and none for the one after it |
|
||||||
|
|
||||||
|
The foreign-endpoint packet was dropped, as such a packet is at any time, and it
|
||||||
|
released the session without receiving a reply. Whether it advanced the session's
|
||||||
|
received count before the release is **not** observable here: no reply was ever
|
||||||
|
emitted for that session again, so no statistic carries the answer. Finding S-1's
|
||||||
|
result that a foreign-endpoint packet is not counted was measured on a **live**
|
||||||
|
session, where a later reply exposed the count, and it does not transfer to this
|
||||||
|
expired-session path. The same
|
||||||
|
release-without-a-final-reply outcome was produced by a truncated echo, an
|
||||||
|
oversized echo, a close from a foreign endpoint, and a burst of unrelated
|
||||||
|
ordinary opens.
|
||||||
|
|
||||||
|
Two scope limits are stated deliberately. These are the classes that were tested,
|
||||||
|
and the effect is not claimed for every possible packet. And nothing here
|
||||||
|
describes the order in which a server performs any internal step: what was
|
||||||
|
observed is that the session was gone when the legitimate request arrived, and
|
||||||
|
that in the control, with no intervening packet, it was not.
|
||||||
|
|
||||||
|
Separately, a burst of unrelated **ordinary opens** released an expired session
|
||||||
|
with no packet bearing its token involved at all, while a burst of **no-test**
|
||||||
|
opens did not — consistent with a no-test open creating no session.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION (server spec Sections 16, 18.2;
|
||||||
|
vectors Section 8.3).
|
||||||
|
|
||||||
|
## Finding S-22: Both lifetime margins are fixed additive constants
|
||||||
|
|
||||||
|
Part II measured the idle grace at one timeout and the maximum-duration grace at
|
||||||
|
two maxima. Sweeping both across several configured values shows each margin is a
|
||||||
|
fixed addition, not a proportion, and not an artifact of when the session began.
|
||||||
|
|
||||||
|
Idle session-release sweep, with gaps measured from the last accepted echo. Each
|
||||||
|
probe used a first otherwise-serviceable echo after the gap and an immediate
|
||||||
|
follow-up; the boundary is inferred from whether that follow-up was dropped.
|
||||||
|
|
||||||
|
| Configured idle timeout | Longest gap with follow-up served | Shortest gap with follow-up dropped |
|
||||||
|
|-------------------------|----------------------------------|------------------------------------|
|
||||||
|
| 500 ms | 5.45 s | 5.50 s |
|
||||||
|
| 2 s | 6.95 s | 7.00 s |
|
||||||
|
| 4 s | 8.95 s | 9.00 s |
|
||||||
|
|
||||||
|
At and beyond the observed release boundary, the first otherwise-serviceable
|
||||||
|
post-gap echo may still receive a final lazy-release reply; the immediate
|
||||||
|
follow-up is then dropped. These values are not first-request rejection
|
||||||
|
thresholds.
|
||||||
|
|
||||||
|
Maximum duration, offsets measured from the first **served** echo:
|
||||||
|
|
||||||
|
| Configured maximum duration | Last offset answered normally | First offset carrying Close |
|
||||||
|
|-----------------------------|-------------------------------|-----------------------------|
|
||||||
|
| 500 ms | 2.45 s | 2.50 s |
|
||||||
|
| 1 s | 2.95 s | 3.00 s |
|
||||||
|
| 3 s | 4.95 s | 5.00 s |
|
||||||
|
|
||||||
|
Across the settings swept — idle timeouts of 500 ms, 2 s and 4 s, and maximum
|
||||||
|
durations of 500 ms, 1 s and 3 s — both margins held at five seconds and two
|
||||||
|
seconds respectively, to within the 50 ms probe spacing, and reproduced on all
|
||||||
|
three builds. The idle boundary did not shift with when the session was opened.
|
||||||
|
|
||||||
|
Each margin rests on three measured points. That is enough to rule out a margin
|
||||||
|
proportional to the configured value over the range tested, and it is what the
|
||||||
|
"additive" description means here. It does not establish the margin for every
|
||||||
|
configurable value: a threshold, a cap, or different behavior at much larger or
|
||||||
|
much smaller settings would not have shown up in these sweeps and remains
|
||||||
|
untested.
|
||||||
|
|
||||||
|
These are measured properties of the tested builds, stated with their tolerance.
|
||||||
|
They are upstream policy, not protocol constants, and no requirement in either
|
||||||
|
specification depends on either figure.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens Findings S-4 and
|
||||||
|
S-11 (server spec Sections 15.2, 18.2, 19; vectors Sections 3.2, 8.1).
|
||||||
|
|
||||||
|
## Finding S-23: Maximum duration starts at the first served echo and closes on the first later served echo
|
||||||
|
|
||||||
|
The boundary sweep establishes two distinct points. The **origin** is the first
|
||||||
|
echo request that is actually **served**. The **trigger** is the first echo after
|
||||||
|
the resulting deadline that would otherwise be served.
|
||||||
|
|
||||||
|
| Case | Result |
|
||||||
|
|------|--------|
|
||||||
|
| Session opened 5 s before its first echo | deadline unchanged — the open does not start the clock |
|
||||||
|
| First request oversized, dropped by the maximum-length policy | clock not started; a later served echo starts it |
|
||||||
|
| First request from a foreign source endpoint, dropped | clock not started; a later served echo starts it |
|
||||||
|
| Echo that crosses the deadline | full normal reply with Close set, and **counted** in the statistics it reports |
|
||||||
|
| Deadline-crossing echo that is itself rate-limited | dropped; the **next served** echo carries the Close flag |
|
||||||
|
| Client's next echo, and its close, after the flag | both unanswered — the token is released |
|
||||||
|
|
||||||
|
Two drop classes were tested in the first-request position — oversized and
|
||||||
|
foreign-endpoint — and neither started the clock. That is the extent of the
|
||||||
|
result: the remaining drop classes were not exercised in that position, and no
|
||||||
|
general "any dropped request" rule is claimed. The rate-limit row is a different
|
||||||
|
question, about a request crossing an already-running deadline rather than one
|
||||||
|
that would start it.
|
||||||
|
|
||||||
|
The close-flagged reply itself is in `captures/server-close.pcapng`, frame 36,
|
||||||
|
from Part II; the boundary sweep is a timing measurement with no capture of its
|
||||||
|
own.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; refines Finding S-4 (server spec
|
||||||
|
Section 15.2; vectors Section 3.2).
|
||||||
|
|
||||||
|
## Finding S-24: Repeated tags — last valid wins; invalid cases are limited
|
||||||
|
|
||||||
|
A tag may appear more than once in one open payload. Among individually valid
|
||||||
|
occurrences the **last** takes effect; this was confirmed for ProtocolVersion,
|
||||||
|
Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP and ServerFill.
|
||||||
|
|
||||||
|
| Payload | Result |
|
||||||
|
|---------|--------|
|
||||||
|
| Duration 1 s, then 2 s | Duration 2 s |
|
||||||
|
| Duration 2 s, then 1 s | Duration 1 s |
|
||||||
|
| Clock 1, then 3 | Clock 3 |
|
||||||
|
| Clock 3, then 4 (out of range) | **no reply** |
|
||||||
|
| Duration 1 s, then 0 (invalid) | **no reply** |
|
||||||
|
| Duration 0 (invalid), then 1 s | **no reply** |
|
||||||
|
| ServerFill `rand`, then a 33-character string | **no reply** |
|
||||||
|
|
||||||
|
**Invalid repeated values were tested more narrowly.** An invalid Duration zero
|
||||||
|
occurrence was dropped in both tested positions: valid then invalid, and invalid
|
||||||
|
then valid. The Clock and ServerFill rows each tested an invalid second occurrence
|
||||||
|
and were also dropped. The opposite ordering was not tested for Clock or
|
||||||
|
ServerFill, and invalid duplicate occurrences were not tested for
|
||||||
|
ProtocolVersion, Interval, Length, ReceivedStats, StampAt or DSCP. These results
|
||||||
|
therefore do not establish a parser-wide, position-independent invalid-duplicate
|
||||||
|
rule for every parameter.
|
||||||
|
|
||||||
|
No conforming client emits a repeated tag; the result is recorded because a
|
||||||
|
decoder that stops at the first occurrence, or validates only the occurrence it
|
||||||
|
keeps, can diverge on the measured adversarial inputs.
|
||||||
|
|
||||||
|
Unknown tags were re-tested at 0, 10, 11, 100, 127, 128, 1000 and 2³². Each was
|
||||||
|
ignored with the rest of the payload still parsed, and an open carrying only
|
||||||
|
unknown tags created an ordinary default session. The value of an unknown tag is
|
||||||
|
consumed as a varint; no length-prefixed or string-shaped value form was accepted
|
||||||
|
for one.
|
||||||
|
|
||||||
|
Finally, a rejected open did not poison the next open: after each malformed
|
||||||
|
payload — incomplete trailing varint, tag with no value, length-prefixed string
|
||||||
|
running past the buffer, varint overflow — a well-formed open from the same
|
||||||
|
endpoint immediately afterwards was served normally. This establishes no
|
||||||
|
persistent externally visible effect on the following request; it does not
|
||||||
|
establish whether hidden accounting or temporary state exists.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION (server spec Section 7.2; vectors
|
||||||
|
Section 4.6).
|
||||||
|
|
||||||
|
## Finding S-25: Reply sizing — negative lengths, the MTU cap, and the fatal ceiling
|
||||||
|
|
||||||
|
Three separate results that are easy to conflate.
|
||||||
|
|
||||||
|
**A negative Length is accepted and produces a minimum-size reply.** It is
|
||||||
|
returned in the open reply as sent. Echo replies for such a session are the size
|
||||||
|
of the field block the negotiated parameters imply — the ordinary rule of server
|
||||||
|
spec Section 9.2 applied to a negotiated length below that block. There is no
|
||||||
|
negative-size concept on the wire.
|
||||||
|
|
||||||
|
**On the two interfaces where the cap was reachable, the emitted reply is capped
|
||||||
|
at the bind interface's MTU.** Re-measured across three interfaces of one host:
|
||||||
|
|
||||||
|
| Bind interface (MTU) | negotiate 1500 | 1501 | 2000 | 8000 |
|
||||||
|
|----------------------|----------------|------|------|------|
|
||||||
|
| loopback (16384) | 1500 | 1501 | 2000 | 8000 |
|
||||||
|
| ethernet (1500) | 1500 | **1500** | **1500** | **1500** |
|
||||||
|
| tunnel (1280) | **1280** | **1280** | **1280** | **1280** |
|
||||||
|
|
||||||
|
On the 1500-byte and 1280-byte interfaces the cap is byte-exact — a negotiated
|
||||||
|
1281 produces 1280 — and the open reply still returns the requested value
|
||||||
|
unclamped, which is the Part II Finding S-14 result confirmed on one further
|
||||||
|
interface.
|
||||||
|
|
||||||
|
The loopback row is weaker evidence and is **not** an observation of a
|
||||||
|
16384-byte cap. It shows only that lengths up to 8000 were emitted unclamped. The
|
||||||
|
loopback cap cannot be approached on this host, because a reply beyond roughly
|
||||||
|
9300 bytes ends the server process before any such length is reached — see the
|
||||||
|
next result. **Host-specific:** these are this host's interface MTUs.
|
||||||
|
|
||||||
|
**A length the host cannot transmit terminates the process.** On the same host,
|
||||||
|
whose maximum outbound datagram size is 9216 bytes, replies up to 9216 bytes were
|
||||||
|
sent normally and negotiated lengths producing replies from roughly 9300 bytes
|
||||||
|
upward failed on the first echo and ended the server. With a wildcard bind, a
|
||||||
|
fatal send on the IPv4 listener took the healthy IPv6 listener down with it —
|
||||||
|
sessions on a different address family, unrelated to the offending one, stopped
|
||||||
|
being served at the same instant. **Host-specific:** 9216 is this host's outbound
|
||||||
|
datagram limit, not a protocol figure.
|
||||||
|
|
||||||
|
On the tested host, this failure was reachable in configurations whose effective
|
||||||
|
reply cap exceeded the host's outbound datagram ceiling, including the tested
|
||||||
|
loopback and wildcard cases. The tested 1500- and 1280-byte interface-bound
|
||||||
|
listeners could not reach it because their reply caps were lower. Jumbo interfaces
|
||||||
|
and other platforms were not tested.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; strengthens Findings S-14 and
|
||||||
|
S-15 (server spec Sections 7.3, 9.2, 22.1, 23.5).
|
||||||
|
|
||||||
|
## Finding S-26: Over-MTU requests show an interface-MTU effective length boundary
|
||||||
|
|
||||||
|
Bound to an interface with a 1500-byte MTU, a 3000-byte request was dropped with
|
||||||
|
a configured maximum of 1499 and accepted with a maximum of 1500. On a 1280-byte
|
||||||
|
tunnel the same decision knee was at 1280. The observed maximum-length decision
|
||||||
|
therefore behaved as though its comparison length was capped at the tested
|
||||||
|
interface boundary. Separately, the policy was strict at the ordinary tested
|
||||||
|
sizes: with a maximum of 1000, a 1000-byte request was answered and a 1001-byte
|
||||||
|
request was dropped.
|
||||||
|
|
||||||
|
> **Black-box inference.** Receive-path truncation before the maximum-length
|
||||||
|
> decision is one explanation consistent with the 1499/1500 and 1279/1280
|
||||||
|
> boundaries, but the experiments cannot distinguish it from another mechanism
|
||||||
|
> that yields the same effective comparison length.
|
||||||
|
|
||||||
|
**Host-specific and environment-specific.** This is an observation on the
|
||||||
|
tested host and interfaces, not a wire rule. It does not prescribe a clean
|
||||||
|
server's receive strategy or maximum-length policy. Negotiated Length is not by
|
||||||
|
itself a valid receive-buffer upper bound, because mandatory protocol structure
|
||||||
|
can require a larger datagram.
|
||||||
|
|
||||||
|
**No capture accompanies this finding.** The capture taken during the experiment
|
||||||
|
was recorded on loopback with no maximum-length policy configured, and there all
|
||||||
|
three oversized requests were answered normally at the negotiated length; it
|
||||||
|
demonstrates neither the effective boundary nor a policy drop, and the behavior it does
|
||||||
|
show is already covered by server spec Section 5.5 and vectors Section 5.4. It
|
||||||
|
was reviewed and not admitted. The textual result above is the evidence.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION, environment-specific (server spec
|
||||||
|
Section 9.4).
|
||||||
|
|
||||||
|
## Finding S-27: DSCP marking is per session, with unmarked opens and marked closes
|
||||||
|
|
||||||
|
Four sessions were opened on one listener negotiating 46, 8, 0 and 184, then
|
||||||
|
driven in three interleaved rounds. Every reply's IP header was read from the
|
||||||
|
kernel. Capture: `captures/server-dscp-interleaved.pcapng` (32 packets).
|
||||||
|
|
||||||
|
| Session | Requested | Open reply TOS | Echo reply TOS, rounds 1–3 |
|
||||||
|
|---------|-----------|----------------|-----------------------------|
|
||||||
|
| 1 | 46 | **0x00** | 0x2e, 0x2e, 0x2e |
|
||||||
|
| 2 | 8 | **0x00** | 0x08, 0x08, 0x08 |
|
||||||
|
| 3 | not requested | **0x00** | 0x00, 0x00, 0x00 |
|
||||||
|
| 4 | 184 | **0x00** | 0xb8, 0xb8, 0xb8 |
|
||||||
|
|
||||||
|
For each of these sessions, whose negotiated values are in `0..=255`, every echo
|
||||||
|
reply carried its own session's value verbatim; no reply carried another session's
|
||||||
|
value in any round, and every open reply was unmarked. Negative and out-of-range
|
||||||
|
values are addressed separately below.
|
||||||
|
|
||||||
|
**Server-initiated close replies: measured, but not shown by any admitted
|
||||||
|
capture.** The DSCP experiment recorded that a close-flagged echo reply carries
|
||||||
|
the same in-range session mark as an ordinary one. Neither admitted capture
|
||||||
|
demonstrates it, and a reader checking them will find nothing that does: the
|
||||||
|
interleaved capture above contains no close-flagged reply at all, and
|
||||||
|
`captures/server-close.pcapng` — which does contain one, at frame 36 — is TOS 0
|
||||||
|
throughout, because that session negotiated no DSCP. The result therefore rests
|
||||||
|
on the measurement, not on capture evidence, and the earlier wording that
|
||||||
|
explained it as "an ordinary marked echo reply that also sets the Close flag" has
|
||||||
|
been dropped: that phrasing argued from the flag rather than reporting what was
|
||||||
|
observed. A capture of a close-flagged reply on a session with a nonzero
|
||||||
|
negotiated DSCP would settle it directly and was not taken.
|
||||||
|
|
||||||
|
**On a listener that has already sent marked replies, an open reply is still
|
||||||
|
unmarked — evidenced separately.** This capture does not show that ordering: its
|
||||||
|
four open exchanges are frames 1–8 and its first marked echo reply is frame 10,
|
||||||
|
so no open reply in it follows a marked one. The ordering is instead established
|
||||||
|
by `captures/dscp-values.pcapng` from Part I, where one listener serves two
|
||||||
|
sessions in sequence: frame 4 is a marked echo reply (TOS `0x08`) and frame 7 is
|
||||||
|
the next session's open reply, unmarked, on the same port. The two pieces of
|
||||||
|
evidence are cited separately rather than merged.
|
||||||
|
|
||||||
|
Observed negative and out-of-range DSCP handling is host-specific, not protocol
|
||||||
|
behavior: a requested −1 appeared as TOS 255 on IPv4 and as traffic class 0 on
|
||||||
|
IPv6. A value of 256 or above was negotiated and echoed back, but no echo reply
|
||||||
|
was observed for that session while the server continued serving its other
|
||||||
|
sessions. The black-box result does not identify whether the absence arose while
|
||||||
|
applying the marking, during transmission, or at another stage.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; extends Findings B and C of
|
||||||
|
Part I (server spec Section 20; vectors Sections 10.1–10.3).
|
||||||
|
|
||||||
|
## Finding S-28: The default fill phase is continuous across the tested listeners
|
||||||
|
|
||||||
|
Finding S-16 recorded that the default fill pattern's phase is not reset per
|
||||||
|
packet or per session and carries over between sessions on one listener. It also
|
||||||
|
carries over between **listeners**: in the tested configuration — one server
|
||||||
|
bound to both an IPv4 and an IPv6 listener — successive echo replies drawn from a
|
||||||
|
session on each continued the same repeating pattern without resetting.
|
||||||
|
|
||||||
|
That is the scope of the result: phase continuity across the sessions tested and
|
||||||
|
across those two listeners. It is not established that a single stream is shared
|
||||||
|
by every session of a process in every configuration, and nothing here describes
|
||||||
|
how the bytes are produced.
|
||||||
|
|
||||||
|
**No recommendation is drawn from it.** The default fill is a fixed, public
|
||||||
|
pattern, so phase continuity across sessions discloses nothing about another
|
||||||
|
peer's traffic — unlike the no-fill mode of Finding S-16's second paragraph and
|
||||||
|
server spec Section 13.4, which is a real disclosure risk and carries its own
|
||||||
|
requirement there. A compatible server may reset the phase, continue it, or
|
||||||
|
derive the payload some other way. Those choices can produce different payload
|
||||||
|
bytes, making the phase policy observable in a packet capture, but they are
|
||||||
|
interoperability-equivalent because a conforming client cannot depend on payload
|
||||||
|
phase. No fill-state arrangement is prescribed.
|
||||||
|
|
||||||
|
**Classification:** SERVER-ONLY NEW INFORMATION; extends Finding S-16 (server spec
|
||||||
|
Section 13.3).
|
||||||
|
|
||||||
|
## Part IV Version Comparison
|
||||||
|
|
||||||
|
Every result in this part was checked against the 0.9.0 release and the tested
|
||||||
|
development build wherever a reproduction check was meaningful. All of the
|
||||||
|
following were **identical** on all three builds:
|
||||||
|
|
||||||
|
| Behavior | 0.9.0 | 0.9.1 | development build |
|
||||||
|
|----------|-------|-------|-------------------|
|
||||||
|
| Open whose restricted StampAt remains non-none and has no Clock is accepted, first echo fatal (S-19) | yes | yes | yes |
|
||||||
|
| Idle boundary at timeout + 5 s (S-22) | yes | yes | yes |
|
||||||
|
| Maximum-duration close at maximum + 2 s (S-23) | yes | yes | yes |
|
||||||
|
| A rate-limited request keeps the session alive (S-20) | yes | yes | yes |
|
||||||
|
| Interface-MTU effective length boundary (S-26) | yes | yes | yes |
|
||||||
|
| Burst allowance behavior | yes | yes | yes |
|
||||||
|
| Fill phase continuous across the tested listeners (S-28) | yes | yes | yes |
|
||||||
|
|
||||||
|
No new version difference was found. The single known wire-visible difference
|
||||||
|
between these builds remains the single-clock midpoint layout of Findings S-7 and
|
||||||
|
S-18, which this pass re-observed unchanged and which is not restated here.
|
||||||
|
|
||||||
|
## Part IV Captures Created
|
||||||
|
|
||||||
|
| File | Packets | Contents |
|
||||||
|
|------|---------|----------|
|
||||||
|
| `captures/server-clock-absent.pcapng` | 3 | An open selecting timestamps with no Clock tag is answered with a token and no Clock parameter; the first echo is the last packet in the exchange |
|
||||||
|
| `captures/server-expiry-consumption.pcapng` | 14 | Control versus foreign-endpoint-first, in one file: whether the packet that releases an expired session emits a final reply |
|
||||||
|
| `captures/server-dscp-interleaved.pcapng` | 32 | Four interleaved sessions with distinct DSCP values over three rounds; no cross-session leakage; unmarked open replies |
|
||||||
|
|
||||||
|
Each was container-rewritten to the convention already used for the captures of
|
||||||
|
Parts I–III — packet records and the rewriting tool's version string only, with
|
||||||
|
no capture comment, host, user, interface or filter metadata and no filesystem
|
||||||
|
path. Packet bytes and per-frame timestamps were verified identical before and
|
||||||
|
after, and the packet counts above are unchanged from the originals.
|
||||||
|
|
||||||
|
Two further captures from this pass were **not** admitted. One was materially
|
||||||
|
redundant with `captures/server-close.pcapng`, which already shows the
|
||||||
|
close-flagged reply, the trigger being counted, and the following packet
|
||||||
|
unanswered; a single-maximum capture cannot evidence the additive-margin result,
|
||||||
|
which is the new part. The other is described under Finding S-26. In both cases
|
||||||
|
the textual result is the evidence, which is the better trade.
|
||||||
|
|
||||||
|
## Part IV Client Spec Change Applied
|
||||||
|
|
||||||
|
| Section | Change | Class |
|
||||||
|
|---------|--------|-------|
|
||||||
|
| 19.3 | Maximum-duration deadline origin is the first echo request that is **served**; the close trigger is the first later echo that would otherwise be served. The two tested first-request drops do not start the clock, and a deadline-crossing rate-limited echo does not carry Close. The 2-second margin is additive across configured maxima | **CLARIFICATION** |
|
||||||
|
|
||||||
|
The client specification and the server specification are both compatibility
|
||||||
|
baselines, so the lifecycle rule is stated the same way in each. The earlier
|
||||||
|
client-side measurements are unaffected — the first request was served in those
|
||||||
|
runs — and no client behavior depends on the distinction, since a conforming
|
||||||
|
client stops sending at the negotiated duration and never reaches the deadline.
|
||||||
|
|
||||||
|
## Part IV Summary
|
||||||
|
|
||||||
|
- New upstream robustness defect documented: **S-19**, an accepted open that is
|
||||||
|
fatal on its first echo, reachable in the default configuration from two
|
||||||
|
unauthenticated datagrams, present in all three builds.
|
||||||
|
- Statements strengthened from thin or single-point evidence: the idle grace and
|
||||||
|
maximum-duration grace (**S-22**), the maximum-duration origin (**S-23**), the
|
||||||
|
rate-limit/idle interaction (**S-20**), the reply-length cap (**S-25**), and the
|
||||||
|
fill-phase scope (**S-28**).
|
||||||
|
- New parser-edge results: repeated tags and unknown-tag value encoding
|
||||||
|
(**S-24**), and the presence-versus-explicit-zero distinction that **S-19**
|
||||||
|
turns on.
|
||||||
|
- No result contradicted any statement in Parts I–III. **S-23** narrows the
|
||||||
|
origin recorded in Finding S-4 from "the first echo request" to "the first
|
||||||
|
echo request that is served"; the earlier measurement is unaffected, because
|
||||||
|
the first request was served in that experiment.
|
||||||
|
- Host-specific figures — interface MTUs, the 9216-byte outbound datagram limit,
|
||||||
|
the effective-length decision knees, and the socket handling of out-of-range DSCP — are
|
||||||
|
labelled as such wherever they appear and are not protocol constants.
|
||||||
|
- Nothing in this part rests on anything but observed request bytes, reply bytes,
|
||||||
|
reply presence, IP header markings, timings, and whether the server was still
|
||||||
|
serving afterwards.
|
||||||
@@ -773,11 +773,20 @@ The following configuration parameters affect protocol behavior:
|
|||||||
produce the wire byte) or the raw 0–255 byte is a presentation decision for
|
produce the wire byte) or the raw 0–255 byte is a presentation decision for
|
||||||
that project, to be made and implemented separately. It is not a protocol
|
that project, to be made and implemented separately. It is not a protocol
|
||||||
fact and this specification does not prescribe it.
|
fact and this specification does not prescribe it.
|
||||||
- **Protocol:** The server echoes packets with the negotiated DSCP value.
|
- **Protocol:** For negotiated values in `0..=255`, the server echoes packets
|
||||||
The server MAY disallow DSCP and reset it to 0.
|
with that DSCP value. The server MAY disallow DSCP and reset it to 0.
|
||||||
|
Negative and out-of-range handling is host-specific; see
|
||||||
|
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 20.
|
||||||
- **Observed behavior:** DSCP/TOS is applied only to echo request and echo
|
- **Observed behavior:** DSCP/TOS is applied only to echo request and echo
|
||||||
reply packets. Open and close packets use TOS=0 regardless of the
|
reply packets. Open and close packets use TOS=0 regardless of the
|
||||||
negotiated DSCP value. [**Verified 2026-04-28** — Finding C.]
|
negotiated DSCP value. [**Verified 2026-04-28** — Finding C.]
|
||||||
|
- **Scope of the previous bullet (clarified 2026-08-12).** "Open and close
|
||||||
|
packets" means **standalone** datagrams: the open request, the open reply, and
|
||||||
|
the client's close request. A **server-initiated close** is not one of them —
|
||||||
|
it is an ordinary echo reply that additionally sets the Close flag, and it
|
||||||
|
carries the session's in-range negotiated marking like any other echo reply. A
|
||||||
|
client must not expect such a reply to arrive unmarked. See
|
||||||
|
`IRTT_SERVER_PROTOCOL_SPEC.md` Sections 15.2 and 20.
|
||||||
|
|
||||||
### 10.9 Open Timeouts
|
### 10.9 Open Timeouts
|
||||||
|
|
||||||
@@ -1518,12 +1527,29 @@ maximum default is 76 bytes (both+both+both+HMAC).
|
|||||||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||||||
|
|
||||||
**Result:** Yes. A server that enforces a maximum test duration answers the
|
**Result:** Yes. A server that enforces a maximum test duration answers the
|
||||||
first echo request arriving after its hard deadline (the configured maximum
|
first otherwise-serviceable echo request after its hard deadline with a complete,
|
||||||
duration plus a 2-second grace, measured from the session's first echo request)
|
ordinary echo reply that
|
||||||
with a complete, ordinary echo reply that additionally has the Close flag set,
|
additionally has the Close flag set, and removes the session at the same moment.
|
||||||
and removes the session at the same moment. Measured: a 1-second maximum
|
The deadline is the configured maximum duration plus a 2-second grace, measured
|
||||||
produced a close-flagged reply 3.01 s after the first echo; a 2-second maximum
|
from the session's first echo request that is actually **served**. Measured: a
|
||||||
produced one at 4.06 s.
|
1-second maximum produced a close-flagged reply 3.01 s after the first echo; a
|
||||||
|
2-second maximum produced one at 4.06 s.
|
||||||
|
|
||||||
|
*Refined 2026-08-12.* This entry previously said the deadline was measured from
|
||||||
|
the session's first echo request, and that the first request after the deadline
|
||||||
|
carries the flag. A later boundary sweep narrowed both ends. For the **origin**,
|
||||||
|
a request that is dropped — the two classes tested were exceeding the maximum
|
||||||
|
length and arriving from a foreign source endpoint — does not start the clock,
|
||||||
|
and a later served echo does. For the **trigger**, a deadline-crossing request
|
||||||
|
that was rate-limited did not carry the flag; the next served echo did. The
|
||||||
|
corresponding behavior for other independently dropped post-deadline request
|
||||||
|
classes was not tested and is not specified here. The earlier measurements above
|
||||||
|
are unaffected, because their first request was served and their triggering
|
||||||
|
request was too. The same sweep showed the 2-second margin to be additive
|
||||||
|
across configured maxima of 500 ms, 1 s and 3 s. See
|
||||||
|
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2 and
|
||||||
|
`BLACKBOX_VERIFICATION_REPORT.md` Finding S-23; nothing a client does depends on
|
||||||
|
the distinction, since a conforming client never reaches the deadline.
|
||||||
|
|
||||||
A conforming client never reaches this deadline, because it stops sending at the
|
A conforming client never reaches this deadline, because it stops sending at the
|
||||||
negotiated duration. The mechanism exists to bound sessions from clients that
|
negotiated duration. The mechanism exists to bound sessions from clients that
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# IRTT-Compatible Server Protocol Specification
|
# IRTT-Compatible Server Protocol Specification
|
||||||
|
|
||||||
**Version:** 1.1-verified
|
**Version:** 1.2-verified
|
||||||
**Date:** 2026-08-11
|
**Date:** 2026-08-12
|
||||||
**Compatibility baseline:** IRTT 0.9.1, protocol version 1
|
**Compatibility baseline:** IRTT 0.9.1, protocol version 1
|
||||||
**Verification platform:** macOS Darwin 25.5.0 arm64
|
**Verification platform:** macOS Darwin 25.5.0 arm64
|
||||||
|
|
||||||
@@ -28,17 +28,23 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT",
|
|||||||
"RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as interoperability
|
"RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as interoperability
|
||||||
requirements, consistent with RFC 2119.
|
requirements, consistent with RFC 2119.
|
||||||
|
|
||||||
Five distinct levels of statement are used throughout, and they are not
|
Six distinct levels of statement are used throughout, and they are not
|
||||||
interchangeable. Nothing in one level implies anything at another.
|
interchangeable. Nothing in one level implies anything at another.
|
||||||
|
|
||||||
| Marker | Meaning |
|
| Marker | Meaning |
|
||||||
|--------|---------|
|
|--------|---------|
|
||||||
| **Protocol requirement** | Required for interoperability with existing clients. Stated with MUST / MUST NOT / SHOULD. Every one rests on observed behavior. |
|
| **Protocol requirement** | Required for interoperability with existing clients. Stated with MUST / MUST NOT / SHOULD. Every one rests on observed behavior. |
|
||||||
| **Upstream 0.9.1 behavior** | What the reference server was observed to do on the wire. A compatible implementation is **not** obliged to reproduce it unless a separate protocol requirement says so. |
|
| **Upstream 0.9.1 behavior** | What the reference server was observed to do on the wire. A compatible implementation is **not** obliged to reproduce it unless a separate protocol requirement says so. Includes the observed robustness defects of Section 22, which a compatible implementation specifically should **not** reproduce. |
|
||||||
|
| **Black-box inference** | A conclusion or explanatory hypothesis drawn from several observations rather than directly observed on the wire. Always labelled as an inference and accompanied by its supporting observations. It may describe a mechanism consistent with the evidence, but does not assert that mechanism as measured fact. Never load-bearing: no requirement depends on one. |
|
||||||
| **Server policy** | A configurable or arbitrary choice of the reference server, including its defaults. A compatible implementation may choose differently, and in several cases should. |
|
| **Server policy** | A configurable or arbitrary choice of the reference server, including its defaults. A compatible implementation may choose differently, and in several cases should. |
|
||||||
| **Robustness recommendation** | Advice for the clean implementation's own resilience or safety. It is **not** an interoperability requirement and no existing client can observe whether it was followed. |
|
| **Robustness recommendation** | Advice for the clean implementation's own resilience or safety. It is **not** an interoperability requirement. Its consequences may nevertheless be externally observable, particularly when malformed or adversarial traffic affects unrelated conforming sessions. |
|
||||||
| **Source-assisted historical conclusion** | A judgement about upstream's release history reached with contaminated-side access to upstream material. Never load-bearing: no requirement in this document depends on one. Used once, in Section 11.3.1. |
|
| **Source-assisted historical conclusion** | A judgement about upstream's release history reached with contaminated-side access to upstream material. Never load-bearing: no requirement in this document depends on one. Used once, in Section 11.3.1. |
|
||||||
|
|
||||||
|
Several statements are additionally marked **host-specific** or
|
||||||
|
**environment-specific**. Those record what one tested host and its interfaces
|
||||||
|
did. They are never protocol constants, and the figures in them must not be
|
||||||
|
compiled into an implementation.
|
||||||
|
|
||||||
Where the reference server's own behavior would be a poor choice for a new
|
Where the reference server's own behavior would be a poor choice for a new
|
||||||
implementation — an unbounded resource policy, a fatal reaction to a single
|
implementation — an unbounded resource policy, a fatal reaction to a single
|
||||||
failed send — this document records the observation and the recommendation
|
failed send — this document records the observation and the recommendation
|
||||||
@@ -181,8 +187,7 @@ produces these accept/discard decisions is conformant, whatever order it
|
|||||||
evaluates them in.
|
evaluates them in.
|
||||||
|
|
||||||
**Discard conditions.** Each of the following was independently observed to
|
**Discard conditions.** Each of the following was independently observed to
|
||||||
cause the datagram to be **silently discarded**, with no reply of any kind and
|
cause the datagram to be **silently discarded**, with no reply of any kind:
|
||||||
no change to the state of any live session:
|
|
||||||
|
|
||||||
| Condition | Notes |
|
| Condition | Notes |
|
||||||
|-----------|-------|
|
|-----------|-------|
|
||||||
@@ -193,12 +198,20 @@ no change to the state of any live session:
|
|||||||
| Authentication mismatch of any kind | Section 14; includes the flag/key mismatch in either direction |
|
| Authentication mismatch of any kind | Section 14; includes the flag/key mismatch in either direction |
|
||||||
| A non-open datagram too short to carry the token | |
|
| A non-open datagram too short to carry the token | |
|
||||||
| A non-open datagram whose token matches no live session | Section 8 |
|
| A non-open datagram whose token matches no live session | Section 8 |
|
||||||
| A non-open datagram whose token matches a session bound to a different source endpoint | Section 8; the session itself is unaffected |
|
| A non-open datagram whose token matches a session bound to a different source endpoint | Section 8; see also the expired-session case in Section 18.2 |
|
||||||
| An echo datagram too short to carry the sequence number | |
|
| An echo datagram too short to carry the sequence number | |
|
||||||
| An echo datagram exceeding a configured maximum packet length | Section 9.4, server policy |
|
| An echo datagram exceeding a configured maximum packet length | Section 9.4, server policy |
|
||||||
| An echo datagram arriving with no rate allowance left | Section 9.3, server policy |
|
| An echo datagram arriving with no rate allowance left | Section 9.3, server policy |
|
||||||
| An open datagram whose parameter payload is malformed or out of range | Section 7.2 |
|
| An open datagram whose parameter payload is malformed or out of range | Section 7.2 |
|
||||||
|
|
||||||
|
**Tested externally visible effects.** Every tested discarded echo left the
|
||||||
|
reported reception count and received window unchanged. A rate-limited request
|
||||||
|
is the known exception to any broader claim that a discard has no externally
|
||||||
|
observable session effect: it extends the observed idle lifetime. Section 9.3
|
||||||
|
contains the detailed comparison. No general claim about hidden state, or about
|
||||||
|
the effects of every discard class at an expiry boundary, is made here; Section
|
||||||
|
18.2 records the tested expired-session release cases.
|
||||||
|
|
||||||
**Protocol requirement.** Every rejection is a **silent discard**. The protocol
|
**Protocol requirement.** Every rejection is a **silent discard**. The protocol
|
||||||
defines no error reply, no reset, and no NACK. A client distinguishes "rejected"
|
defines no error reply, no reset, and no NACK. A client distinguishes "rejected"
|
||||||
from "lost" only by timing out. A compatible server MUST NOT answer any of the
|
from "lost" only by timing out. A compatible server MUST NOT answer any of the
|
||||||
@@ -331,9 +344,12 @@ A server parses the whole parameter payload. Observed acceptance rules:
|
|||||||
| Duration present and ≤ 0 | **Dropped, no reply** |
|
| Duration present and ≤ 0 | **Dropped, no reply** |
|
||||||
| Interval present and ≤ 0 | **Dropped, no reply** |
|
| Interval present and ≤ 0 | **Dropped, no reply** |
|
||||||
| Duration or Interval absent (implicitly 0) | Accepted |
|
| Duration or Interval absent (implicitly 0) | Accepted |
|
||||||
| ReceivedStats outside 0–3 | **Dropped, no reply** |
|
| ReceivedStats outside 0–3, or negative | **Dropped, no reply** |
|
||||||
| StampAt outside 0–4 | **Dropped, no reply** |
|
| StampAt outside 0–4, or negative | **Dropped, no reply** |
|
||||||
| Clock outside 1–3 | **Dropped, no reply** |
|
| Clock outside 1–3, **including an explicit 0** | **Dropped, no reply** |
|
||||||
|
| Clock **absent** with requested StampAt = none | Accepted; no timestamps are emitted |
|
||||||
|
| Clock **absent** with requested StampAt ≠ none, where restriction leaves StampAt non-none | The **open** is answered normally, with a valid token. The session it creates is not usable: the first echo request on it is fatal to the tested upstream server. See Section 22.5 |
|
||||||
|
| Clock **absent** with requested StampAt ≠ none, where restriction changes StampAt to none | Accepted; the session serves normally without timestamps. Observed with the no-timestamp server policy (Sections 11.4 and 22.5). |
|
||||||
| Length any value, including negative | Accepted (see Section 7.3) |
|
| Length any value, including negative | Accepted (see Section 7.3) |
|
||||||
| DSCP any value, including negative or > 255 | Accepted (see Section 7.3) |
|
| DSCP any value, including negative or > 255 | Accepted (see Section 7.3) |
|
||||||
| ServerFill string longer than 32 bytes | **Dropped, no reply** |
|
| ServerFill string longer than 32 bytes | **Dropped, no reply** |
|
||||||
@@ -350,6 +366,74 @@ parameter payloads. A payload of over 200 bytes composed of unknown tags was
|
|||||||
accepted and answered normally. Any such limit in an implementation is a local
|
accepted and answered normally. Any such limit in an implementation is a local
|
||||||
buffer-sizing choice, not a protocol rule.
|
buffer-sizing choice, not a protocol rule.
|
||||||
|
|
||||||
|
**Wire fact and upstream 0.9.1 behavior.** The two Clock rows above are the
|
||||||
|
sharpest instance of a general point: an *omitted* tag and an explicitly encoded
|
||||||
|
`0` are distinct encodings, and upstream produces distinct outcomes for them.
|
||||||
|
An explicit `Clock = 0` is out of range and the open is dropped; an omitted Clock
|
||||||
|
is accepted.
|
||||||
|
|
||||||
|
**Compatibility consequence.** An implementation that chooses to reproduce
|
||||||
|
those distinct upstream outcomes needs enough information during request
|
||||||
|
validation to distinguish the two forms.
|
||||||
|
|
||||||
|
**Clean policy.** A clean server MAY intentionally map otherwise distinct
|
||||||
|
adversarial forms to the same compatible, safe outcome when no conforming client
|
||||||
|
depends on the distinction. This does not prescribe any decoded representation
|
||||||
|
or require retaining presence after validation.
|
||||||
|
|
||||||
|
The two directions differ, and the distinction must not be over-applied. In a
|
||||||
|
**request**, a present zero is an ordinary encoding a client may send: `Length =
|
||||||
|
0` and `DSCP = 0` are both accepted, and `Clock = 0` is rejected precisely
|
||||||
|
*because* it is present carrying that value. In an **open reply**, a parameter
|
||||||
|
whose restricted value is zero is omitted entirely (Section 7.4), so a server
|
||||||
|
never emits a present-as-zero form — which is why a client reconstructs an absent
|
||||||
|
parameter as zero when reading a reply. An implementation reproducing the
|
||||||
|
upstream request outcomes must not treat an omitted request parameter as an
|
||||||
|
explicit zero during validation.
|
||||||
|
|
||||||
|
**Upstream 0.9.1 behavior — repeated occurrences of a known tag.** A parameter
|
||||||
|
tag may appear more than once in one payload. Among occurrences that are each
|
||||||
|
individually valid, the **last** one takes effect; this was measured for
|
||||||
|
ProtocolVersion, Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP
|
||||||
|
and ServerFill.
|
||||||
|
|
||||||
|
**Invalid repeated values were tested more narrowly.** For Duration, an invalid
|
||||||
|
zero occurrence caused the open to be dropped in both tested positions: valid
|
||||||
|
then invalid, and invalid then valid. For Clock and ServerFill, an invalid second
|
||||||
|
occurrence also caused the open to be dropped. The opposite ordering was not
|
||||||
|
tested for Clock or ServerFill, and invalid duplicate occurrences were not tested
|
||||||
|
for ProtocolVersion, Interval, Length, ReceivedStats, StampAt or DSCP. The
|
||||||
|
evidence therefore does not establish a parser-wide, position-independent
|
||||||
|
invalid-duplicate rule for every parameter. See the vectors file, Section 4.6.
|
||||||
|
|
||||||
|
**Protocol requirement.** None: no conforming client emits a repeated tag. A
|
||||||
|
compatible server MAY resolve repeats however it likes. The behavior is recorded
|
||||||
|
because a decoder that stops at the first occurrence, or that validates only the
|
||||||
|
occurrence it keeps, can diverge from the measured upstream outcomes on these
|
||||||
|
adversarial inputs.
|
||||||
|
|
||||||
|
**Upstream 0.9.1 behavior — unknown tags.** The value of an unknown tag is
|
||||||
|
consumed as a varint and discarded. Tag numbers 0, 10, 11, 100, 127, 128, 1000
|
||||||
|
and 2³² were each tested and each ignored, with the remainder of the payload
|
||||||
|
still parsed and the open answered normally; an open carrying *only* unknown tags
|
||||||
|
creates an ordinary default session. No length-prefixed or string-shaped value
|
||||||
|
form was accepted for an unknown tag.
|
||||||
|
|
||||||
|
> **Black-box inference, not an observation.** Because the only value encoding
|
||||||
|
> accepted for an unknown tag is a varint, an unknown tag cannot carry a byte
|
||||||
|
> string, and the wire format therefore offers no general extension channel for
|
||||||
|
> structured values. That is a conclusion drawn from the encodings tested above,
|
||||||
|
> not a documented property, and this specification does not define an extension
|
||||||
|
> mechanism.
|
||||||
|
|
||||||
|
**Upstream 0.9.1 behavior — a rejected open does not poison the next open.**
|
||||||
|
After each tested malformed payload was dropped — incomplete trailing varint, a
|
||||||
|
tag with no value, a length-prefixed string running past the end of the buffer,
|
||||||
|
or a varint overflow — a well-formed open sent immediately afterwards from the
|
||||||
|
same endpoint was accepted and served normally. The experiment establishes no
|
||||||
|
persistent externally visible parser or session effect on the following request;
|
||||||
|
it does not establish whether any hidden accounting or temporary state exists.
|
||||||
|
|
||||||
### 7.3 Server Restriction of Parameters
|
### 7.3 Server Restriction of Parameters
|
||||||
|
|
||||||
A server MAY reduce or replace requested parameters. The restricted values are
|
A server MAY reduce or replace requested parameters. The restricted values are
|
||||||
@@ -386,7 +470,23 @@ client aborts; idle timeout 4 s produces no reduction at all for a 1 s request.
|
|||||||
enforced minimum remains the larger. A fully conforming client then sends at the
|
enforced minimum remains the larger. A fully conforming client then sends at the
|
||||||
negotiated interval and is rate-limited (Section 9.3). Observed with a 5 s
|
negotiated interval and is rate-limited (Section 9.3). Observed with a 5 s
|
||||||
minimum interval and an 8 s idle timeout: negotiated interval 2 s, but only the
|
minimum interval and an 8 s idle timeout: negotiated interval 2 s, but only the
|
||||||
first request in each 5 s window was answered.
|
first request in each 5 s window was answered. The floor is applied before the
|
||||||
|
cap, so the returned interval can also fall *below* a configured minimum:
|
||||||
|
measured with a 5 s minimum interval and a 4 s idle timeout, the returned
|
||||||
|
interval was 1 s. Further boundary rows are in the vectors file, Section 4.4.
|
||||||
|
|
||||||
|
**Upstream 0.9.1 behavior — a negative Length.** A negative Length is accepted
|
||||||
|
during negotiation and returned in the open reply as the value the client sent.
|
||||||
|
There is no negative-size concept on the wire: echo replies for such a session
|
||||||
|
are the size of the field block the negotiated parameters imply, which is what
|
||||||
|
the ordinary reply-length rule of Section 9.2 produces for any negotiated length
|
||||||
|
at or below that block. Clamping happens at the reply, not at the negotiation.
|
||||||
|
|
||||||
|
**No requirement follows from this.** That upstream accepts a negative Length
|
||||||
|
does not make acceptance an interoperability requirement, and this specification
|
||||||
|
does not decide whether a clean server should accept, clamp or reject one. No
|
||||||
|
conforming client emits a negative Length, and a client that did would compare
|
||||||
|
the returned value against what it requested in the ordinary way.
|
||||||
|
|
||||||
### 7.4 Open Reply
|
### 7.4 Open Reply
|
||||||
|
|
||||||
@@ -491,12 +591,12 @@ Observed outcomes:
|
|||||||
| Case | Result |
|
| Case | Result |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Correct token, original endpoint | Served normally |
|
| Correct token, original endpoint | Served normally |
|
||||||
| Correct token, **different source port**, same IP | **Dropped, no reply.** Session left intact and still usable from the original port |
|
| Correct token, **different source port**, same IP | **Dropped, no reply.** For a live, non-expired session, it remains usable from the original port; see Section 18.2 for the separately observed expired-session release case. |
|
||||||
| Correct token, different address family | **Dropped, no reply** (different listener, unknown token there) |
|
| Correct token, different address family | **Dropped, no reply** (different listener, unknown token there) |
|
||||||
| Unknown token | **Dropped, no reply** |
|
| Unknown token | **Dropped, no reply** |
|
||||||
| Zero token | **Dropped, no reply** (treated as any other unknown token) |
|
| Zero token | **Dropped, no reply** (treated as any other unknown token) |
|
||||||
| Token of a session already closed | **Dropped, no reply** |
|
| Token of a session already closed | **Dropped, no reply** |
|
||||||
| **Close** request bearing a valid token from a foreign source port | **Dropped**; the session is **not** closed |
|
| **Close** request bearing a valid token from a foreign source port | **Dropped**; it does not close an otherwise live, non-expired session. Section 18.2 covers the separately observed expired-session release case. |
|
||||||
|
|
||||||
The last row matters: endpoint binding protects the close path as well as the
|
The last row matters: endpoint binding protects the close path as well as the
|
||||||
echo path. A capture of these cases is in
|
echo path. A capture of these cases is in
|
||||||
@@ -606,10 +706,39 @@ produced 1500-byte replies, while the open reply still reported the requested
|
|||||||
value. Upstream clients treat a reply shorter than the negotiated length as a
|
value. Upstream clients treat a reply shorter than the negotiated length as a
|
||||||
fatal error, so this configuration fails on the first echo. See Section 22.
|
fatal error, so this configuration fails on the first echo. See Section 22.
|
||||||
|
|
||||||
|
Re-measured across three interfaces of one host:
|
||||||
|
|
||||||
|
| Bind interface (MTU) | 1500 | 1501 | 2000 | 8000 |
|
||||||
|
|----------------------|------|------|------|------|
|
||||||
|
| loopback (16384) | 1500 | 1501 | 2000 | 8000 |
|
||||||
|
| ethernet (1500) | 1500 | **1500** | **1500** | **1500** |
|
||||||
|
| tunnel (1280) | **1280** | **1280** | **1280** | **1280** |
|
||||||
|
|
||||||
|
On the **1500-byte and 1280-byte** interfaces the cap was that interface's MTU
|
||||||
|
exactly, byte for byte — a negotiated 1281 produced 1280 — and a negotiated
|
||||||
|
length at or below the MTU was emitted in full.
|
||||||
|
|
||||||
|
The loopback row establishes something weaker, and is not evidence of an exact
|
||||||
|
16384-byte cap. All it shows is that lengths up to 8000 were emitted unclamped.
|
||||||
|
The cap could not be approached there: on this host a reply beyond roughly 9300
|
||||||
|
bytes ends the server process (Section 22.1), well below the loopback MTU, so no
|
||||||
|
observation of that boundary is possible on this configuration.
|
||||||
|
|
||||||
|
This is a property of the tested host and its interfaces, not a wire constant.
|
||||||
|
Whether the cap is always the listener interface's MTU on other platforms remains
|
||||||
|
open; see Section 23.5. Note also that the tested loopback and wildcard
|
||||||
|
configurations allowed a negotiated length to exceed what this host will actually
|
||||||
|
transmit — the hazard of Section 22.1.
|
||||||
|
|
||||||
### 9.3 Rate Limiting
|
### 9.3 Rate Limiting
|
||||||
|
|
||||||
**Server policy.** The reference server enforces the negotiated minimum send
|
**Server policy.** The tested upstream rate limiter replenishes reply allowance
|
||||||
interval with a token-bucket allowance:
|
using the configured minimum send interval. This may differ from the interval
|
||||||
|
returned during negotiation when the idle-timeout restriction of Section 7.3
|
||||||
|
reduces the negotiated value below that configured minimum.
|
||||||
|
|
||||||
|
**Black-box inference.** The observed burst and replenishment behavior is
|
||||||
|
consistent with a token-bucket allowance:
|
||||||
|
|
||||||
- a burst allowance of *N* requests is available immediately (default N = 5);
|
- a burst allowance of *N* requests is available immediately (default N = 5);
|
||||||
- allowance is replenished at one request per configured minimum interval and
|
- allowance is replenished at one request per configured minimum interval and
|
||||||
@@ -623,7 +752,61 @@ were all answered. With a burst of 1, a blast of 4 produced 1 reply.
|
|||||||
**Protocol requirement.** A rate-limited request MUST NOT be answered and MUST
|
**Protocol requirement.** A rate-limited request MUST NOT be answered and MUST
|
||||||
NOT affect the reception statistics (Section 10). Measured: the received count
|
NOT affect the reception statistics (Section 10). Measured: the received count
|
||||||
advanced 1, 2, 3, 4, 5 across the accepted burst and continued 6, 7, 8 for the
|
advanced 1, 2, 3, 4, 5 across the accepted burst and continued 6, 7, 8 for the
|
||||||
later requests — dropped requests were not counted.
|
later requests — dropped requests were not counted. Neither the count nor the
|
||||||
|
received window advances for a rate-limited request.
|
||||||
|
|
||||||
|
**Upstream 0.9.1 behavior — a rate-limited request still counts as activity for
|
||||||
|
idle expiry.** This was the only tested drop class that did. A rate-limited request
|
||||||
|
receives no reply and advances neither statistic, yet it extends the session's
|
||||||
|
observed idle lifetime exactly as a served request would. Every other drop class
|
||||||
|
tested leaves the idle deadline where it was:
|
||||||
|
|
||||||
|
| Injected packet | Counted / window advanced | Extends the observed idle lifetime |
|
||||||
|
|-----------------|---------------------------|------------------------------------|
|
||||||
|
| Bad magic | no | no |
|
||||||
|
| Invalid flag bits | no | no |
|
||||||
|
| Reply or Open flag on a data packet | no | no |
|
||||||
|
| Unknown token, or a zero token | no | no |
|
||||||
|
| Valid token from a foreign source endpoint | no | no — but see Section 18.2 |
|
||||||
|
| Echo truncated before the sequence number | no | no |
|
||||||
|
| Request over the configured maximum length | no | no |
|
||||||
|
| Bad, missing or zeroed authentication | no | no |
|
||||||
|
| **Request with no rate allowance left** | **no** | **YES** |
|
||||||
|
|
||||||
|
The rate-limit row was established three ways against each other, and reproduced
|
||||||
|
on the 0.9.0 release, the 0.9.1 release and the tested post-0.9.1 development
|
||||||
|
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
|
### 9.4 Maximum Request Length
|
||||||
|
|
||||||
@@ -640,6 +823,29 @@ request is dropped while opens still succeed. Measured with a maximum of 10: the
|
|||||||
session opened, negotiated a length of 10, and every 16-byte echo request was
|
session opened, negotiated a length of 10, and every 16-byte echo request was
|
||||||
dropped.
|
dropped.
|
||||||
|
|
||||||
|
The comparison is strict and is made against the length the server **received**.
|
||||||
|
Measured with a maximum of 1000: a 1000-byte request was answered and a 1001-byte
|
||||||
|
request was dropped.
|
||||||
|
|
||||||
|
**Environment-specific observation — an over-MTU request shows an
|
||||||
|
interface-MTU effective length boundary.** On the tested host, a 3000-byte
|
||||||
|
request sent to a 1500-byte-MTU interface was dropped with a configured maximum
|
||||||
|
of 1499 and accepted with a maximum of 1500. On a 1280-byte tunnel interface the
|
||||||
|
same decision knee appeared at 1280. In these experiments, the maximum-length
|
||||||
|
decision therefore behaved as though its comparison length was capped at the
|
||||||
|
tested interface boundary.
|
||||||
|
|
||||||
|
> **Black-box inference.** Receive-path truncation before the maximum-length
|
||||||
|
> decision is one explanation consistent with this observation, but the
|
||||||
|
> experiment cannot distinguish it from receiving the full datagram and
|
||||||
|
> normalizing or capping the comparison length, or from another mechanism that
|
||||||
|
> yields the same effective boundary.
|
||||||
|
|
||||||
|
This is an observation on the tested host and interfaces, not a wire rule. It
|
||||||
|
does not constrain a clean implementation's receive strategy or maximum-length
|
||||||
|
policy. Negotiated Length is not by itself a valid receive-buffer upper bound,
|
||||||
|
because mandatory protocol structure can require a larger datagram.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Sequence Handling and Received Statistics
|
## 10. Sequence Handling and Received Statistics
|
||||||
@@ -1132,10 +1338,31 @@ seq 3 payload: 74 74 69 72 74 74 69
|
|||||||
|
|
||||||
and across separate sessions on the same listener, the phase carried over.
|
and across separate sessions on the same listener, the phase carried over.
|
||||||
|
|
||||||
|
**The continuity extends across listeners, not only across sessions on one
|
||||||
|
listener.** In the tested configuration — one server bound to both an IPv4 and an
|
||||||
|
IPv6 listener — successive echo replies drawn from a session on each continued
|
||||||
|
the same repeating pattern without resetting. So the observable phase is
|
||||||
|
continuous across the sessions tested and across those two listeners.
|
||||||
|
|
||||||
|
That is the extent of what was measured. It is not established here that a single
|
||||||
|
stream is shared by *every* session of a process in every configuration, and no
|
||||||
|
claim is made about how any implementation produces the bytes.
|
||||||
|
|
||||||
**Protocol requirement.** A client MUST NOT assume a reply payload begins at the
|
**Protocol requirement.** A client MUST NOT assume a reply payload begins at the
|
||||||
start of the fill pattern, and MUST NOT use payload bytes for any protocol
|
start of the fill pattern, and MUST NOT use payload bytes for any protocol
|
||||||
purpose. A compatible server MAY reset the phase per packet.
|
purpose. A compatible server MAY reset the phase per packet.
|
||||||
|
|
||||||
|
**No recommendation follows from the phase observation alone.** The default fill
|
||||||
|
is a fixed, public pattern, so its phase carrying across sessions discloses
|
||||||
|
nothing about another peer's traffic — unlike the no-fill mode of Section 13.4,
|
||||||
|
which is a genuine disclosure risk and carries its own requirement there. A
|
||||||
|
compatible server may reset the phase per packet, continue it, or derive the
|
||||||
|
payload some other way. The resulting payload bytes can differ, making the
|
||||||
|
phase policy observable in a packet capture, but the difference has no protocol
|
||||||
|
significance: these choices are interoperability-equivalent because a
|
||||||
|
conforming client cannot depend on payload phase. This specification prescribes
|
||||||
|
no particular arrangement of fill state.
|
||||||
|
|
||||||
### 13.4 No Fill
|
### 13.4 No Fill
|
||||||
|
|
||||||
**Upstream 0.9.1 behavior.** When the effective fill is `none`, no bytes are
|
**Upstream 0.9.1 behavior.** When the effective fill is `none`, no bytes are
|
||||||
@@ -1251,7 +1478,7 @@ and no payload.
|
|||||||
| Trailing bytes after the token | Accepted; session removed |
|
| Trailing bytes after the token | Accepted; session removed |
|
||||||
| Repeated close with the same token | Dropped as an unknown token; no reply |
|
| Repeated close with the same token | Dropped as an unknown token; no reply |
|
||||||
| Echo after close | Dropped as an unknown token; no reply |
|
| Echo after close | Dropped as an unknown token; no reply |
|
||||||
| Close from a foreign source port | Dropped; session **not** removed |
|
| Close from a foreign source port | Dropped; does not remove an otherwise live, non-expired session. See Section 18.2 for the separately observed expired-session release case. |
|
||||||
| Close bearing an unknown or zero token | Dropped; no reply |
|
| Close bearing an unknown or zero token | Dropped; no reply |
|
||||||
|
|
||||||
**Protocol requirement.** A server MUST accept the close request and release the
|
**Protocol requirement.** A server MUST accept the close request and release the
|
||||||
@@ -1294,10 +1521,16 @@ duration hard limit:
|
|||||||
- when a maximum test duration is configured, the server clamps the negotiated
|
- when a maximum test duration is configured, the server clamps the negotiated
|
||||||
duration to it during negotiation;
|
duration to it during negotiation;
|
||||||
- it additionally enforces a hard deadline of **maximum duration + 2 seconds**,
|
- it additionally enforces a hard deadline of **maximum duration + 2 seconds**,
|
||||||
measured from the **first echo request** of the session;
|
measured from the session's **first echo request that is actually served**;
|
||||||
- the first echo request arriving after that deadline is answered with a normal,
|
- the first echo request arriving after that deadline **that would otherwise be
|
||||||
complete echo reply — correct sequence number, token, statistics and
|
served** is answered with a normal, complete echo reply — correct sequence
|
||||||
timestamps — with the Close flag additionally set;
|
number, token, statistics and timestamps — with the Close flag additionally
|
||||||
|
set;
|
||||||
|
- in the one tested dropped-trigger case, a deadline-crossing echo that was
|
||||||
|
rate-limited was silently dropped without Close. The session remained usable,
|
||||||
|
and the next served echo carried Close;
|
||||||
|
- the corresponding behavior for other independently dropped post-deadline
|
||||||
|
request classes was not tested and is not specified here;
|
||||||
- the session is removed at the same time, so every later request is dropped as
|
- the session is removed at the same time, so every later request is dropped as
|
||||||
an unknown token.
|
an unknown token.
|
||||||
|
|
||||||
@@ -1306,6 +1539,43 @@ first echo; maximum duration 2 s → close flag at 4.06 s and again at 4.1 s in
|
|||||||
separate run. Captured in `captures/server-close.pcapng` (frame 36 carries
|
separate run. Captured in `captures/server-close.pcapng` (frame 36 carries
|
||||||
flags `0x06`).
|
flags `0x06`).
|
||||||
|
|
||||||
|
Re-measured as a boundary sweep, the 2-second margin is additive across the
|
||||||
|
configured maximum rather than proportional to it. Offsets are measured from the
|
||||||
|
first served echo:
|
||||||
|
|
||||||
|
| Configured maximum duration | Last offset still answered normally | First offset carrying Close |
|
||||||
|
|-----------------------------|-------------------------------------|-----------------------------|
|
||||||
|
| 500 ms | 2.45 s | 2.50 s |
|
||||||
|
| 1 s | 2.95 s | 3.00 s |
|
||||||
|
| 3 s | 4.95 s | 5.00 s |
|
||||||
|
|
||||||
|
Four further properties of the trigger were measured, and each is externally
|
||||||
|
visible:
|
||||||
|
|
||||||
|
- **The clock starts at the first *served* echo, not at open and not at the first
|
||||||
|
echo that merely arrives.** Opening a session five seconds before its first
|
||||||
|
echo did not move the deadline. Two drop classes were tested as the first
|
||||||
|
request and neither started the clock: an **oversized** first request, and a
|
||||||
|
first request from a **foreign source endpoint**. In both cases a later served
|
||||||
|
echo became the origin instead. Whether the other drop classes of Section 5.4
|
||||||
|
behave the same way was not tested, and no general rule about them is stated
|
||||||
|
here.
|
||||||
|
- **The triggering echo is included in the reception statistics it reports.** The
|
||||||
|
close-flagged reply carries the count and window that request produced, exactly
|
||||||
|
as an ordinary reply would.
|
||||||
|
- **A request that would trigger the close but is itself rate-limited is simply
|
||||||
|
dropped.** The close is not lost: it is carried by the next echo that is served
|
||||||
|
once the rate allowance refills.
|
||||||
|
- **The token is unusable from that reply onward.** The client's next echo, and
|
||||||
|
the close request it sends in response to the flag, both go unanswered.
|
||||||
|
|
||||||
|
The close-flagged reply itself is captured in `captures/server-close.pcapng`,
|
||||||
|
frame 36; the sweep above is a timing measurement and has no capture of its own.
|
||||||
|
The margin is a property of the tested builds, stated with a measurement
|
||||||
|
tolerance of one probe interval (50 ms in the sweep above). It is upstream
|
||||||
|
policy, not a protocol constant, and none of the protocol requirements below
|
||||||
|
depends on its value.
|
||||||
|
|
||||||
A conforming client never reaches this deadline, because it stops sending at the
|
A conforming client never reaches this deadline, because it stops sending at the
|
||||||
negotiated duration. The mechanism exists to bound sessions from clients that
|
negotiated duration. The mechanism exists to bound sessions from clients that
|
||||||
ignore the negotiated duration.
|
ignore the negotiated duration.
|
||||||
@@ -1356,6 +1626,7 @@ token**, and the fully restricted parameter payload.
|
|||||||
| Subsequent close with the zero token | Dropped, no reply |
|
| Subsequent close with the zero token | Dropped, no reply |
|
||||||
| With authentication | Reply is authenticated normally; flags `0x0F` |
|
| With authentication | Reply is authenticated normally; flags `0x0F` |
|
||||||
| Cleanup required | None |
|
| Cleanup required | None |
|
||||||
|
| Effect on *other* sessions' expiry | **None observed.** A burst of no-test opens did not release an unrelated expired session, while a burst of ordinary opens did (Section 18.2) |
|
||||||
|
|
||||||
**Protocol requirement.** A server MUST NOT create a usable session for a
|
**Protocol requirement.** A server MUST NOT create a usable session for a
|
||||||
no-test open, and MUST return a zero token. The Close flag in the reply here
|
no-test open, and MUST return a zero token. The Close flag in the reply here
|
||||||
@@ -1367,8 +1638,10 @@ specification, Sections 6.2 and 8.3.
|
|||||||
|
|
||||||
## 17. Malformed, Unexpected and Adversarial Packets
|
## 17. Malformed, Unexpected and Adversarial Packets
|
||||||
|
|
||||||
Compact reference table. "Session effect: none" means the referenced session (if
|
Compact reference table. For rows whose session state is **live**, "Session
|
||||||
any) is left fully usable.
|
effect: none" means the referenced session remains usable. The separately
|
||||||
|
observed expired-session release cases are scoped to the **expired** row and
|
||||||
|
Section 18.2.
|
||||||
|
|
||||||
| Request condition | Session state | Auth state | Server response | Session effect |
|
| Request condition | Session state | Auth state | Server response | Session effect |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
@@ -1399,8 +1672,8 @@ any) is left fully usable.
|
|||||||
| Echo shorter than the negotiated length | live | ok | **reply at negotiated length** | normal |
|
| Echo shorter than the negotiated length | live | ok | **reply at negotiated length** | normal |
|
||||||
| Echo with a duplicate sequence number | live | ok | **reply** | count +1, window unchanged |
|
| Echo with a duplicate sequence number | live | ok | **reply** | count +1, window unchanged |
|
||||||
| Echo with a lower sequence number than the last | live | ok | **reply** | count +1, window reset to `0x1` |
|
| Echo with a lower sequence number than the last | live | ok | **reply** | count +1, window reset to `0x1` |
|
||||||
| Echo after the maximum-duration deadline | live | ok | **reply with Close flag** | session removed |
|
| Otherwise-serviceable echo after the maximum-duration deadline | live | ok | **normal echo reply with Close flag** | request counted; session removed (a rate-limited trigger is dropped; see Section 15.2) |
|
||||||
| Echo after idle expiry deadline | expired | ok | **reply** (served once) | session removed |
|
| Otherwise-serviceable echo that is the first tested packet to touch an expired session | expired | ok | **reply** (served once), then session removed | see Section 18.2 for normally dropped packets that can release it first |
|
||||||
| Any request on a closed / expired session | gone | ok | drop | none |
|
| Any request on a closed / expired session | gone | ok | drop | none |
|
||||||
| Close with trailing bytes | live | ok | **accepted**; no reply, as for any close | session removed |
|
| Close with trailing bytes | live | ok | **accepted**; no reply, as for any close | session removed |
|
||||||
| Echo-shaped datagram with the Open flag set, otherwise admissible | — | ok | treated as an **open**; the token bytes are parsed as parameters | new session if those bytes parse |
|
| Echo-shaped datagram with the Open flag set, otherwise admissible | — | ok | treated as an **open**; the token bytes are parsed as parameters | new session if those bytes parse |
|
||||||
@@ -1439,7 +1712,7 @@ default **1 minute**, with `0` meaning "never expire".
|
|||||||
|
|
||||||
**Observed behavior**, measured with a 2-second idle timeout:
|
**Observed behavior**, measured with a 2-second idle timeout:
|
||||||
|
|
||||||
| Gap since the previous accepted echo | First request after the gap | The request after that |
|
| Gap since the previous accepted echo | First otherwise-serviceable echo after the gap | Immediate follow-up |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 5.0 s | answered | answered |
|
| 5.0 s | answered | answered |
|
||||||
| 6.5 s | answered | answered |
|
| 6.5 s | answered | answered |
|
||||||
@@ -1450,14 +1723,96 @@ default **1 minute**, with `0` meaning "never expire".
|
|||||||
|
|
||||||
Two things follow, and both are externally observable:
|
Two things follow, and both are externally observable:
|
||||||
|
|
||||||
1. **The effective expiry deadline is the configured idle timeout plus a
|
1. **The observed idle release boundary is the configured idle timeout plus a
|
||||||
5-second grace period.** Measured boundary between 6.9 s and 7.1 s for a
|
5-second grace period.** Measured boundary between 6.9 s and 7.1 s for a
|
||||||
2-second timeout.
|
2-second timeout.
|
||||||
2. **The first request that arrives after the deadline is still answered**, and
|
2. **In observed upstream behavior, the first otherwise-serviceable echo that
|
||||||
the session is torn down as part of handling it. Only the *next* request is
|
reaches an expired session receives a final reply.** The immediate follow-up
|
||||||
dropped. A compatible implementation MAY drop the first post-deadline request
|
is then dropped, which is how release is observed externally.
|
||||||
instead; clients cannot depend on either behavior, because from their side
|
|
||||||
the deadline coincides with an unresponsive server.
|
The qualification matters, and the paragraphs below are where it comes from.
|
||||||
|
A final post-deadline reply is emitted only if the packet that first triggers
|
||||||
|
the expired session's release is itself an otherwise-serviceable echo. If a
|
||||||
|
packet that would normally be dropped — a foreign-endpoint echo, a truncated
|
||||||
|
or oversized one — reaches the expired session first, it may release the
|
||||||
|
session without receiving a reply; no later request then receives that final
|
||||||
|
reply.
|
||||||
|
|
||||||
|
**The grace is a fixed additive margin, not a proportion of the timeout.** A
|
||||||
|
boundary sweep across three configured timeouts, three repetitions each, located
|
||||||
|
the observed session-release boundary at timeout + 5 s in every case, to within
|
||||||
|
the 50 ms probe spacing. Each probe used a first otherwise-serviceable echo after
|
||||||
|
the gap and an immediate follow-up:
|
||||||
|
|
||||||
|
| Configured idle timeout | Longest gap with follow-up served | Shortest gap with follow-up dropped |
|
||||||
|
|-------------------------|----------------------------------|------------------------------------|
|
||||||
|
| 500 ms | 5.45 s | 5.50 s |
|
||||||
|
| 2 s | 6.95 s | 7.00 s |
|
||||||
|
| 4 s | 8.95 s | 9.00 s |
|
||||||
|
|
||||||
|
At and beyond this boundary, the first otherwise-serviceable post-gap echo may
|
||||||
|
still receive upstream's final lazy-release reply; the immediate follow-up is
|
||||||
|
then dropped. These values are therefore not thresholds at which the first
|
||||||
|
post-gap echo is rejected.
|
||||||
|
|
||||||
|
The same boundary was observed on the 0.9.0 release, the 0.9.1 release and the
|
||||||
|
tested post-0.9.1 development build. Because the extra margin is the same five
|
||||||
|
seconds at every timeout, it is not proportional to the timeout, and the boundary
|
||||||
|
did not shift with when the session was opened. Nothing here describes how a
|
||||||
|
server notices that a session has expired; only when it stops serving one is
|
||||||
|
observable, and only that is stated.
|
||||||
|
|
||||||
|
**Expiry is observable as a lazy release.** A final post-deadline reply is
|
||||||
|
emitted only when the packet that first triggers the expired session's release is
|
||||||
|
itself otherwise serviceable; it is not reserved for a later request from the
|
||||||
|
session's client. Measured against a control in the same capture
|
||||||
|
(`captures/server-expiry-consumption.pcapng`):
|
||||||
|
|
||||||
|
- **Control.** A session left idle past its deadline answered the client's next
|
||||||
|
echo, then dropped the one after it.
|
||||||
|
- **Released by a foreign endpoint.** A second session, otherwise identical, was
|
||||||
|
touched just past its deadline by an echo bearing its token from a *different
|
||||||
|
source port*. That packet was dropped as a foreign-endpoint request, as it
|
||||||
|
would be at any time — but it released the session without a reply. The
|
||||||
|
legitimate client's own post-deadline echo, arriving 0.3 s later, then received
|
||||||
|
**no reply at all**, and neither did the one after it; no final reply remained.
|
||||||
|
|
||||||
|
The same effect was produced by a truncated echo, an oversized echo, a close
|
||||||
|
request from a foreign endpoint, and a burst of unrelated ordinary opens arriving
|
||||||
|
before the legitimate client's post-deadline request.
|
||||||
|
|
||||||
|
Two limits on how far this generalises. First, these are the request classes that
|
||||||
|
were tested; this specification does not claim that *every* packet touching an
|
||||||
|
expired token has the effect, only these. Second, it says nothing about the order
|
||||||
|
in which a server performs any internal step — what is observable is that the
|
||||||
|
session was gone by the time the legitimate request arrived, and that in the
|
||||||
|
control, with no intervening packet, it was not.
|
||||||
|
|
||||||
|
**A burst of ordinary opens released an expired session; a burst of no-test opens
|
||||||
|
did not.** A burst of normal open requests was observed to release an unrelated
|
||||||
|
expired session, so a session can also stop being served without any packet
|
||||||
|
bearing its token arriving at all. A burst of no-test opens (Section 16) had no
|
||||||
|
such effect — consistent with a no-test open creating no session of its own.
|
||||||
|
|
||||||
|
Only the burst was tested. Whether a **single** ordinary open is enough, or
|
||||||
|
whether the effect depends on a count, a rate, or some other property of the
|
||||||
|
batch, was not measured and is not claimed here.
|
||||||
|
|
||||||
|
> **Black-box inference.** Taken together, these observations are consistent with
|
||||||
|
> expiry being evaluated when a session is looked up and, additionally, when a new
|
||||||
|
> session is created, rather than by a scheduled removal that a client could
|
||||||
|
> observe as a distinct event. That is an inference from the timing and packet
|
||||||
|
> outcomes above, not a directly measured description of the mechanism, and no
|
||||||
|
> requirement in this document depends on it.
|
||||||
|
|
||||||
|
**Compatibility significance.** The lazy-release behavior is externally
|
||||||
|
observable. A conforming client may encounter it after a sufficiently long
|
||||||
|
mid-test client-to-server path interruption and observe whether the first
|
||||||
|
otherwise-serviceable echo after recovery receives a reply.
|
||||||
|
|
||||||
|
**Clean policy remains open.** Whether a clean server reproduces upstream lazy
|
||||||
|
release or expires immediately is a separate implementation decision. The
|
||||||
|
current evidence does not introduce a new RFC 2119 requirement for that choice.
|
||||||
|
|
||||||
**Upstream 0.9.1 behavior — a session that has never carried an echo request
|
**Upstream 0.9.1 behavior — a session that has never carried an echo request
|
||||||
never expires.** The idle clock starts at the first echo request, not at open.
|
never expires.** The idle clock starts at the first echo request, not at open.
|
||||||
@@ -1476,7 +1831,8 @@ them resident indefinitely as upstream does.
|
|||||||
|
|
||||||
Requests dropped for rate limiting still refresh the idle clock, so a client
|
Requests dropped for rate limiting still refresh the idle clock, so a client
|
||||||
sending faster than the allowance keeps its session alive without receiving
|
sending faster than the allowance keeps its session alive without receiving
|
||||||
replies.
|
replies. Section 9.3 records the full drop-class comparison establishing this,
|
||||||
|
and that no other tested drop class has the same effect.
|
||||||
|
|
||||||
### 18.3 Resource Limits
|
### 18.3 Resource Limits
|
||||||
|
|
||||||
@@ -1538,9 +1894,9 @@ implementation.
|
|||||||
| Default minimum send interval | 10 ms | policy |
|
| Default minimum send interval | 10 ms | policy |
|
||||||
| Default burst allowance | 5 requests | policy |
|
| Default burst allowance | 5 requests | policy |
|
||||||
| Default idle timeout | 1 minute | policy |
|
| Default idle timeout | 1 minute | policy |
|
||||||
| Idle-expiry grace beyond the timeout | 5 seconds (measured) | policy |
|
| Idle-expiry grace beyond the timeout | 5 seconds, additive (measured at timeouts of 500 ms, 2 s and 4 s) | policy |
|
||||||
| Maximum negotiable interval | idle timeout ÷ 4 (measured) | policy |
|
| Maximum negotiable interval | idle timeout ÷ 4 (measured) | policy |
|
||||||
| Maximum-duration grace | 2 seconds (measured) | policy |
|
| Maximum-duration grace | 2 seconds, additive, from the first served echo (measured at maxima of 500 ms, 1 s and 3 s) | policy |
|
||||||
| Default maximum test duration | unlimited | policy |
|
| Default maximum test duration | unlimited | policy |
|
||||||
| Default maximum packet length | unlimited | policy |
|
| Default maximum packet length | unlimited | policy |
|
||||||
| Default fill pattern | repeating `69 72 74 74` | policy |
|
| Default fill pattern | repeating `69 72 74 74` | policy |
|
||||||
@@ -1558,11 +1914,11 @@ would produce a smaller interval makes the server unusable by those clients.
|
|||||||
|
|
||||||
**Protocol requirement — what the wire parameter is.** The DSCP parameter
|
**Protocol requirement — what the wire parameter is.** The DSCP parameter
|
||||||
(tag 8) carries the **raw IP TOS / Traffic Class byte**, with a useful range of
|
(tag 8) carries the **raw IP TOS / Traffic Class byte**, with a useful range of
|
||||||
**0–255**. It is not a 6-bit codepoint. A server applies the negotiated value
|
**0–255**. It is not a 6-bit codepoint. For negotiated values in that range, a
|
||||||
directly as the TOS / Traffic Class byte of its echo replies, and a compatible
|
server applies the value directly as the TOS / Traffic Class byte of its echo
|
||||||
implementation MUST encode and interpret it the same way. The common Expedited
|
replies, and a compatible implementation MUST encode and interpret it the same
|
||||||
Forwarding marking is therefore **0xb8 (184)** on the wire, not 46. Verified by
|
way. The common Expedited Forwarding marking is therefore **0xb8 (184)** on the
|
||||||
capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
|
wire, not 46. Verified by capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
|
||||||
`captures/dscp-session.pcapng`.
|
`captures/dscp-session.pcapng`.
|
||||||
|
|
||||||
> **Out of scope: user-facing DSCP notation.** Whether an implementation's
|
> **Out of scope: user-facing DSCP notation.** Whether an implementation's
|
||||||
@@ -1573,28 +1929,108 @@ capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
|
|||||||
> upstream wire behavior, and it does not belong in this specification.
|
> upstream wire behavior, and it does not belong in this specification.
|
||||||
|
|
||||||
**Server policy.** When DSCP is permitted and supported on the socket, the server
|
**Server policy.** When DSCP is permitted and supported on the socket, the server
|
||||||
applies the negotiated value to its echo replies. When DSCP is disallowed, the
|
applies an in-range negotiated value to its echo replies. When DSCP is disallowed,
|
||||||
negotiated value is forced to 0 and returned as absent from the parameter
|
the negotiated value is forced to 0 and returned as absent from the parameter
|
||||||
payload.
|
payload.
|
||||||
|
|
||||||
**Observed range handling.** The negotiated value is **not** range-checked
|
**Observed range handling on the tested host.** Values in `0..=255` were applied
|
||||||
during negotiation. Values 0–255 are applied normally. A value above 255 is
|
normally. A value of 256 or above was accepted and echoed back in the open reply,
|
||||||
accepted and echoed back in the open reply, but then every echo reply for that
|
but no echo replies were observed for that session. Other sessions continued to
|
||||||
session fails to send and is dropped — the client sees a session that opens and
|
be served normally throughout.
|
||||||
then answers nothing.
|
|
||||||
|
The black-box observation does not identify whether the absence arose while
|
||||||
|
applying the requested socket marking, during transmission, or at another stage.
|
||||||
|
It is distinct from the non-transient outbound reply send failure of Sections 6.3
|
||||||
|
and 22.1, whose observed result is process termination.
|
||||||
|
|
||||||
|
**Observed marking scope, measured on interleaved sessions.** Four sessions were
|
||||||
|
opened on one listener negotiating 46, 8, 0 and 184, and then driven in three
|
||||||
|
interleaved rounds of one echo each. Every echo reply carried its own session's
|
||||||
|
value as the TOS byte, verbatim, in all three rounds, and no reply carried
|
||||||
|
another session's value. The marking therefore follows the session, not the
|
||||||
|
listener or the most recent negotiation. Captured in
|
||||||
|
`captures/server-dscp-interleaved.pcapng`.
|
||||||
|
|
||||||
|
**Observed marking of the other packet kinds.**
|
||||||
|
|
||||||
|
| Packet | Marking |
|
||||||
|
|--------|---------|
|
||||||
|
| Open reply | **Always unmarked** (TOS 0) |
|
||||||
|
| Echo reply | The session's negotiated value, verbatim for values in `0..=255` (see the out-of-range observations below) |
|
||||||
|
| Server-initiated close (Section 15.2) | Measured to carry the same session marking for values in `0..=255`. No admitted capture shows this — see the note below |
|
||||||
|
| Client-initiated close | No reply is sent at all (Section 15.1) |
|
||||||
|
|
||||||
|
An unmarked open reply is not merely the state of a freshly started listener: in
|
||||||
|
`captures/dscp-values.pcapng` a listener that had already emitted a marked echo
|
||||||
|
reply answered a subsequent open on the same port with TOS 0 (frame 4 carries
|
||||||
|
TOS `0x08`; the open reply at frame 7 is unmarked). The interleaved capture above
|
||||||
|
does not show this ordering — all four of its open exchanges precede its first
|
||||||
|
marked echo reply — so the two are cited separately.
|
||||||
|
|
||||||
|
**The close-reply row rests on a measurement, not on a capture.** No capture in
|
||||||
|
`captures/` shows a close-flagged reply on a session with a nonzero negotiated
|
||||||
|
DSCP. The interleaved capture contains no close-flagged reply, and
|
||||||
|
`captures/server-close.pcapng`, which does contain one at frame 36, is TOS 0
|
||||||
|
throughout because that session negotiated no DSCP. Treat the row as the observed
|
||||||
|
result it is, and not as a deduction from the Close flag being "just a flag on an
|
||||||
|
ordinary echo reply" — nothing here rules out a marking difference on that path
|
||||||
|
beyond the measurement itself. A capture of a marked session reaching its
|
||||||
|
maximum-duration deadline would settle it and was not taken.
|
||||||
|
|
||||||
|
**Observed handling of negative and out-of-range DSCP values on the tested host.**
|
||||||
|
A negative value behaved differently by address family. One negative value was
|
||||||
|
measured with its emitted marking recorded: a negotiated **−1** appeared on the
|
||||||
|
wire as **TOS 255** on IPv4, while the same negotiation on IPv6 produced a
|
||||||
|
traffic class of **0**. A value of 256 or above produced the no-reply result
|
||||||
|
described above.
|
||||||
|
|
||||||
|
A single negative data point does not identify the transformation. Taking the low
|
||||||
|
byte of the value's two's-complement representation — equivalently, the value
|
||||||
|
modulo 256 — and a special case for this input both predict TOS 255, and no
|
||||||
|
experiment distinguishes them. Plain clamping into `0..=255` is the one candidate
|
||||||
|
the observation does rule out: it would have emitted 0. This specification
|
||||||
|
records the observed mapping only and does not name a rule.
|
||||||
|
|
||||||
|
**This is platform behavior, not protocol behavior.** The observed handling of
|
||||||
|
negative and out-of-range values is host-specific, and a compatible server is
|
||||||
|
under no obligation to reproduce it. Do not read the v4/v6 difference as a wire
|
||||||
|
rule.
|
||||||
|
|
||||||
**Robustness recommendation (clean implementation).** Reject or clamp DSCP
|
**Robustness recommendation (clean implementation).** Reject or clamp DSCP
|
||||||
values outside 0–255 during negotiation rather than accepting a value that cannot
|
values outside `0..=255` during negotiation rather than accepting a negotiated
|
||||||
be applied, so that the value returned in the open reply is one the server will
|
value that leads to an observed no-reply session, so that the value returned in
|
||||||
honour.
|
the open reply is one the server will reliably honour. Which of the two —
|
||||||
|
rejection or clamping — and how the clean server exposes the value are decisions
|
||||||
|
for the implementing project, not settled here.
|
||||||
|
|
||||||
DSCP is applied to echo replies only; open and close packets are unmarked (see
|
**What "close packets are unmarked" does and does not mean.** The client
|
||||||
the client specification, Section 10.8).
|
specification, Section 10.8, records that open and close packets carry TOS 0.
|
||||||
|
That statement is about **standalone** open and close datagrams — the client's
|
||||||
|
open request, the server's open reply, and the client's close request — and it
|
||||||
|
holds: each was observed unmarked regardless of the negotiated value.
|
||||||
|
|
||||||
|
It does **not** cover a server-initiated close, because there is no standalone
|
||||||
|
close datagram in that direction. A server signals close by setting the Close
|
||||||
|
flag on an ordinary echo reply (Section 15.2), and that reply carries the
|
||||||
|
session's in-range negotiated marking like every other echo reply. A server must
|
||||||
|
not clear the marking merely because the Close flag is set, and a clean
|
||||||
|
implementation should not read "close packets are unmarked" as applying to it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 21. Version Differences
|
## 21. Version Differences
|
||||||
|
|
||||||
|
Only one wire-visible difference between the tested builds has been found, and it
|
||||||
|
is the subject of Section 21.1. The behaviors most likely to be mistaken for
|
||||||
|
version-specific quirks were re-measured against all three builds — the 0.9.0
|
||||||
|
release, the 0.9.1 release and the tested post-0.9.1 development tree — and were
|
||||||
|
**identical** on each: the accepted-then-fatal open of Section 22.5; the idle
|
||||||
|
boundary at timeout + 5 s; the maximum-duration close at maximum + 2 s; a
|
||||||
|
rate-limited request refreshing the idle lifetime; the interface-MTU effective
|
||||||
|
length boundary; the burst allowance; and fill-phase continuity across the tested
|
||||||
|
listeners. Where a later section names a single build, that is because only that
|
||||||
|
build was measured for the statement in question.
|
||||||
|
|
||||||
### 21.1 0.9.0 → 0.9.1
|
### 21.1 0.9.0 → 0.9.1
|
||||||
|
|
||||||
Two independent things are recorded here and must not be conflated.
|
Two independent things are recorded here and must not be conflated.
|
||||||
@@ -1678,9 +2114,18 @@ reproduce. They are listed because each is reachable from ordinary client
|
|||||||
traffic, so a clean server has to decide what to do instead.
|
traffic, so a clean server has to decide what to do instead.
|
||||||
|
|
||||||
Everything in this section is an **observation plus a robustness
|
Everything in this section is an **observation plus a robustness
|
||||||
recommendation**. None of it is an interoperability requirement: no conforming
|
recommendation**. None of it is an interoperability requirement, in the specific
|
||||||
client can tell which choice a server made, because in every case the visible
|
sense that no conforming client can *provoke* any of these outcomes: reaching
|
||||||
effect is either silence or an ordinary reply.
|
them takes traffic a conforming client does not send, and a server that avoids
|
||||||
|
them is fully compatible with every existing client.
|
||||||
|
|
||||||
|
That is not the same as saying the consequences are invisible. Several of these
|
||||||
|
hazards end the server process, and Sections 22.1 and 22.5 record unrelated
|
||||||
|
conforming clients on the same process losing their sessions at that moment. Those
|
||||||
|
clients plainly observe the outcome — they just cannot cause it, and from their
|
||||||
|
side it is indistinguishable from the server going away for any other reason. The
|
||||||
|
classification rests on what a conforming client can trigger and on nothing being
|
||||||
|
required of the wire, not on a claim that the effects cannot be seen.
|
||||||
|
|
||||||
### 22.1 Oversized Reply Terminates the Server
|
### 22.1 Oversized Reply Terminates the Server
|
||||||
|
|
||||||
@@ -1691,12 +2136,30 @@ process exits. Observed on a host whose maximum outbound datagram size was
|
|||||||
9216 bytes: a session negotiating 15000 bytes killed the server on its first
|
9216 bytes: a session negotiating 15000 bytes killed the server on its first
|
||||||
echo reply, and all subsequent clients received nothing.
|
echo reply, and all subsequent clients received nothing.
|
||||||
|
|
||||||
|
Re-measured on the same host, the boundary sits at that maximum: negotiated
|
||||||
|
lengths producing replies of up to 9216 bytes were sent normally, and lengths
|
||||||
|
producing replies from roughly 9300 bytes upward were fatal on the first echo.
|
||||||
|
Those figures are that host's outbound datagram limit and its interface MTUs;
|
||||||
|
they are **not** protocol constants, and no clean implementation should encode
|
||||||
|
them. On the tested host, this failure was reachable in configurations whose
|
||||||
|
effective reply cap exceeded the host's outbound datagram ceiling, including the
|
||||||
|
tested loopback and wildcard cases. The tested 1500- and 1280-byte interface-bound
|
||||||
|
listeners could not reach it because their reply caps were lower. Jumbo interfaces
|
||||||
|
and other platforms were not tested (Section 23.5).
|
||||||
|
|
||||||
|
**The blast radius is the whole process, not the session or the listener.** With
|
||||||
|
a wildcard bind, a fatal send on the IPv4 listener terminated the process and
|
||||||
|
took the healthy IPv6 listener down with it — sessions with no relationship to
|
||||||
|
the offending one, on a different address family, stopped being served at the
|
||||||
|
same instant.
|
||||||
|
|
||||||
The default configuration places no upper bound on the negotiated length, so
|
The default configuration places no upper bound on the negotiated length, so
|
||||||
this is reachable without any malformed packet.
|
this is reachable without any malformed packet.
|
||||||
|
|
||||||
**Robustness recommendation (restated from Section 6.3).** Treat a per-packet
|
**Robustness recommendation (restated from Section 6.3).** Treat a per-packet
|
||||||
send failure as a per-packet event rather than a fatal one, and refuse to
|
send failure as a per-packet event rather than a fatal one, and refuse to
|
||||||
negotiate a length the server cannot transmit.
|
negotiate a length the server cannot transmit. A failure encountered while
|
||||||
|
serving one session should not stop unrelated sessions or unrelated listeners.
|
||||||
|
|
||||||
### 22.2 Zero-Length Fill Pattern Terminates the Server
|
### 22.2 Zero-Length Fill Pattern Terminates the Server
|
||||||
|
|
||||||
@@ -1729,6 +2192,76 @@ leak bytes from other clients' traffic. The requirement not to emit another
|
|||||||
peer's data is stated in Section 13.4; it is a security property of the
|
peer's data is stated in Section 13.4; it is a security property of the
|
||||||
implementation rather than a wire-compatibility rule.
|
implementation rather than a wire-compatibility rule.
|
||||||
|
|
||||||
|
### 22.5 An Accepted Open With No Clock Parameter Is Fatal On Its First Echo
|
||||||
|
|
||||||
|
**Observed upstream robustness defect.** This is the most severe of the hazards
|
||||||
|
recorded here: it is reachable in the default configuration, from two
|
||||||
|
unauthenticated datagrams, by a peer that never sends anything malformed.
|
||||||
|
|
||||||
|
When the server's restricted (effective) StampAt remains non-none, an open
|
||||||
|
request whose requested StampAt is Send, Receive, Both or Midpoint and which
|
||||||
|
**omits the Clock parameter entirely** is accepted:
|
||||||
|
|
||||||
|
- the open reply is an ordinary session-creating reply, with a valid non-zero
|
||||||
|
token and the restricted parameters;
|
||||||
|
- the reply carries **no** Clock parameter, and no Clock value is synthesized
|
||||||
|
into it;
|
||||||
|
- the **first echo request** on that session receives **no reply**, and the
|
||||||
|
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
|
## 23. Remaining Unknowns
|
||||||
@@ -1807,12 +2340,20 @@ inspected, what was attempted, and what evidence would settle it.
|
|||||||
does it vary with how the platform reports interface MTUs?
|
does it vary with how the platform reports interface MTUs?
|
||||||
- **Why it matters:** It determines the largest length a server can honestly
|
- **Why it matters:** It determines the largest length a server can honestly
|
||||||
negotiate.
|
negotiate.
|
||||||
- **Evidence inspected:** Measured on one host: replies capped at exactly the
|
- **Evidence inspected:** Measured on one host across three interfaces —
|
||||||
bind interface's MTU (1500 on an Ethernet interface); loopback with a
|
loopback (16384), Ethernet (1500) and a tunnel (1280). On the **Ethernet and
|
||||||
16384-byte MTU was masked by a smaller maximum outbound datagram size.
|
tunnel** interfaces the reply was capped at exactly that interface's MTU, byte
|
||||||
- **Attempted:** Two interfaces with different MTUs on one host.
|
for byte (1281 → 1280), and a negotiated length at or below the MTU was emitted
|
||||||
|
in full. The **loopback** interface shows only that lengths up to 8000 were
|
||||||
|
emitted unclamped: its cap cannot be approached on this host, because a reply
|
||||||
|
beyond roughly 9300 bytes ends the server process first (Section 22.1), so no
|
||||||
|
16384-byte boundary was observed and none is claimed.
|
||||||
|
- **Attempted:** Three interfaces with different MTUs on one host.
|
||||||
- **Would resolve it:** The same measurement on Linux and Windows hosts with
|
- **Would resolve it:** The same measurement on Linux and Windows hosts with
|
||||||
jumbo-frame and tunnel interfaces.
|
jumbo-frame and tunnel interfaces. The exact-cap result rests on two interfaces
|
||||||
|
of one host and says nothing about how another platform reports interface MTUs;
|
||||||
|
a host whose maximum outbound datagram size exceeds its loopback MTU would also
|
||||||
|
settle whether the rule holds for a large-MTU interface.
|
||||||
|
|
||||||
### 23.6 Whether any trigger other than the duration limit sets Close on a reply
|
### 23.6 Whether any trigger other than the duration limit sets Close on a reply
|
||||||
|
|
||||||
@@ -1865,7 +2406,9 @@ in Section 24.3 and are deliberately not mixed in.
|
|||||||
6. Return a zero token and the Close flag for a no-test open, and create no
|
6. Return a zero token and the Close flag for a no-test open, and create no
|
||||||
session for it.
|
session for it.
|
||||||
7. Bind each session to its token **and** its source endpoint, and drop packets
|
7. Bind each session to its token **and** its source endpoint, and drop packets
|
||||||
that fail either check without disturbing the session.
|
that fail either check. For a live, non-expired session, that drop leaves the
|
||||||
|
session usable from its bound endpoint; see Section 18.2 for the separately
|
||||||
|
observed expired-session release behavior.
|
||||||
8. Echo the token and sequence number unchanged in every echo reply.
|
8. Echo the token and sequence number unchanged in every echo reply.
|
||||||
9. Emit the negotiated fields in the specified order, and pad to the negotiated
|
9. Emit the negotiated fields in the specified order, and pad to the negotiated
|
||||||
length. For StampAt = Midpoint this means the fields the negotiated Clock
|
length. For StampAt = Midpoint this means the fields the negotiated Clock
|
||||||
@@ -1898,21 +2441,28 @@ in Section 24.3 and are deliberately not mixed in.
|
|||||||
|
|
||||||
### 24.3 Robustness recommendations for the clean implementation
|
### 24.3 Robustness recommendations for the clean implementation
|
||||||
|
|
||||||
Not interoperability requirements. No conforming client can observe whether these
|
Not standalone interoperability requirements. They are here because each
|
||||||
were followed; they are here because each addresses a behavior observed upstream
|
addresses a behavior observed upstream that a new implementation should not
|
||||||
that a new implementation should not copy.
|
copy. Particular failures, including a process-wide failure caused by another
|
||||||
|
peer, can be observable to a conforming client; these recommendations do not
|
||||||
|
choose the clean implementation's specific policy.
|
||||||
|
|
||||||
1. Treat a per-packet send failure as a per-packet event, not a fatal one
|
1. Treat a per-packet send failure as a per-packet event, not a fatal one, and
|
||||||
(Sections 6.3, 22.1).
|
keep a failure while serving one session from stopping unrelated sessions or
|
||||||
|
unrelated listeners (Sections 6.3, 22.1).
|
||||||
2. Operate under a bounded session-resource policy, and expire sessions that have
|
2. Operate under a bounded session-resource policy, and expire sessions that have
|
||||||
never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits
|
never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits
|
||||||
and eviction policy are a clean-project decision.
|
and eviction policy are a clean-project decision.
|
||||||
3. Validate a fill descriptor before use, and reject a zero-length pattern
|
3. Validate a fill descriptor before use, and reject a zero-length pattern
|
||||||
(Section 22.2).
|
(Section 22.2).
|
||||||
4. Reject or clamp DSCP values the socket cannot carry, during negotiation
|
4. Reject or clamp DSCP values outside `0..=255` during negotiation to avoid
|
||||||
(Section 20).
|
accepting a value that leads to an observed no-reply session (Section 20).
|
||||||
5. Clamp the negotiated length to what the server can actually transmit
|
5. Clamp the negotiated length to what the server can actually transmit
|
||||||
(Sections 22.1, 22.3).
|
(Sections 22.1, 22.3).
|
||||||
|
6. Never let a set of session parameters the server accepted at open produce a
|
||||||
|
fatal condition on a later request (Section 22.5). Which resolution to adopt
|
||||||
|
for an open that selects timestamps without a clock is deliberately left to
|
||||||
|
the implementation work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
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
|
**Source:** Black-box observation of an upstream IRTT **0.9.1 release** server on
|
||||||
macOS Darwin 25.5.0 arm64, driven by a raw UDP harness.
|
macOS Darwin 25.5.0 arm64, driven by a raw UDP harness.
|
||||||
**Date:** 2026-08-11
|
**Date:** 2026-08-12
|
||||||
**Companion to:** `../IRTT_SERVER_PROTOCOL_SPEC.md`
|
**Companion to:** `../IRTT_SERVER_PROTOCOL_SPEC.md`
|
||||||
|
|
||||||
These vectors are stated as **input → observed output**. They describe what a
|
These vectors are stated as **input → observed output**. They describe what a
|
||||||
@@ -10,10 +10,12 @@ server was seen to emit for a given request sequence. They do not describe how
|
|||||||
any implementation produces those bytes.
|
any implementation produces those bytes.
|
||||||
|
|
||||||
Unless a row says otherwise, the server under test is the unmodified upstream
|
Unless a row says otherwise, the server under test is the unmodified upstream
|
||||||
0.9.1 release. Two other builds appear only in Section 5.2.3, each named
|
0.9.1 release. Two other builds appear where they are named explicitly: the 0.9.0
|
||||||
explicitly: the 0.9.0 release, and a build of the upstream development tree six
|
release, and a build of the upstream development tree six upstream commits past
|
||||||
upstream commits past the 0.9.1 tag. Results from those builds never stand in for
|
the 0.9.1 tag. Results from those builds never stand in for 0.9.1. Section 5.2.3
|
||||||
0.9.1.
|
is a version *comparison*, where the three builds differ; elsewhere the other two
|
||||||
|
builds are named only to record that a 0.9.1 result reproduced on them unchanged
|
||||||
|
(Sections 3.2, 4.7, 8.1, 8.2).
|
||||||
|
|
||||||
Addresses in the accompanying captures are loopback only. Session tokens are
|
Addresses in the accompanying captures are loopback only. Session tokens are
|
||||||
server-assigned random values and differ per run; where a token appears below it
|
server-assigned random values and differ per run; where a token appears below it
|
||||||
@@ -264,6 +266,38 @@ session:
|
|||||||
|
|
||||||
The deadline is the configured maximum duration plus a 2-second grace period.
|
The deadline is the configured maximum duration plus a 2-second grace period.
|
||||||
|
|
||||||
|
Boundary sweep, probes spaced 50 ms, offsets measured from the first **served**
|
||||||
|
echo of the session:
|
||||||
|
|
||||||
|
| configured maximum duration | last offset answered normally | first offset carrying Close |
|
||||||
|
|-----------------------------|-------------------------------|-----------------------------|
|
||||||
|
| 500 ms | 2.45 s | 2.50 s |
|
||||||
|
| 1 s | 2.95 s | 3.00 s |
|
||||||
|
| 3 s | 4.95 s | 5.00 s |
|
||||||
|
|
||||||
|
The 2-second margin is additive across the configured maximum, not proportional
|
||||||
|
to it. Reproduced on the 0.9.0 release, the 0.9.1 release and the tested
|
||||||
|
post-0.9.1 development build.
|
||||||
|
|
||||||
|
What starts and what carries the deadline:
|
||||||
|
|
||||||
|
| case | result |
|
||||||
|
|------|--------|
|
||||||
|
| Session opened 5 s before its first echo | deadline unchanged — the open does not start it |
|
||||||
|
| First request oversized (dropped by the maximum-length policy) | clock **not** started; a later served echo starts it |
|
||||||
|
| First request from a foreign source endpoint (dropped) | clock **not** started; a later served echo starts it |
|
||||||
|
| Echo that crosses the deadline | full normal reply with Close set; **counted** in the received count and window it reports |
|
||||||
|
| Echo that crosses the deadline but is itself rate-limited | dropped, no reply; the **next served** echo carries the Close flag |
|
||||||
|
| Client's next echo after the close-flagged reply | no reply — token released |
|
||||||
|
| Client's close request after the close-flagged reply | no reply — token released |
|
||||||
|
|
||||||
|
Two drop classes were tested as the session's first request — an oversized
|
||||||
|
request and one from a foreign source endpoint — and neither started the
|
||||||
|
deadline; a later served echo became the origin instead. The other drop classes
|
||||||
|
of Section 9 were not tested in that position, and this vector makes no claim
|
||||||
|
about them. The rate-limit row above concerns a request that crosses an
|
||||||
|
already-running deadline, not one that would start it.
|
||||||
|
|
||||||
### 3.3 Observed upstream client reaction to a close-flagged reply
|
### 3.3 Observed upstream client reaction to a close-flagged reply
|
||||||
|
|
||||||
Driving an upstream 0.9.1 client against a server that sets the Close flag from
|
Driving an upstream 0.9.1 client against a server that sets the Close flag from
|
||||||
@@ -360,6 +394,20 @@ Requesting a 2 s interval against a 4 s idle timeout produces a reduction to
|
|||||||
exactly 1 s: rejected in strict mode as an ordinary restriction, accepted in
|
exactly 1 s: rejected in strict mode as an ordinary restriction, accepted in
|
||||||
loose mode.
|
loose mode.
|
||||||
|
|
||||||
|
Boundary rows with a configured **minimum** interval as well. The requested
|
||||||
|
interval is raised to the minimum first and capped to (idle timeout ÷ 4)
|
||||||
|
afterwards, so the returned value can end up below the configured minimum:
|
||||||
|
|
||||||
|
| server minimum interval | server idle timeout | requested | negotiated |
|
||||||
|
|-------------------------|---------------------|-----------|------------|
|
||||||
|
| 100 ms | 1 min | 10 ms | 100 ms — raised to the minimum |
|
||||||
|
| 100 ms | 1 min | 100 ms | 100 ms — unchanged |
|
||||||
|
| 100 ms | 1 min | 60 s | 15 s — capped at timeout ÷ 4 |
|
||||||
|
| **5 s** | **4 s** | 10 ms | **1 s** — the cap wins, below the configured minimum |
|
||||||
|
| 5 s | 0 (disabled) | 10 ms | 5 s — no cap applies |
|
||||||
|
| 0 | 4 ns | 10 ms | 1 ns |
|
||||||
|
| 0 | 1 ns | 10 ms | **absent from the reply** — the cap is 0, and a restricted value of zero is omitted |
|
||||||
|
|
||||||
### 4.5 Timestamp allowance
|
### 4.5 Timestamp allowance
|
||||||
|
|
||||||
Requested StampAt → negotiated StampAt, by server timestamp allowance:
|
Requested StampAt → negotiated StampAt, by server timestamp allowance:
|
||||||
@@ -372,6 +420,68 @@ Requested StampAt → negotiated StampAt, by server timestamp allowance:
|
|||||||
| 3 (both) | 3 | **4** | 0 |
|
| 3 (both) | 3 | **4** | 0 |
|
||||||
| 4 (midpoint) | 4 | 4 | 0 |
|
| 4 (midpoint) | 4 | 4 | 0 |
|
||||||
|
|
||||||
|
### 4.6 Repeated occurrences of a known tag
|
||||||
|
|
||||||
|
One open request per row, carrying the listed occurrences of a single tag.
|
||||||
|
|
||||||
|
| parameter payload | result |
|
||||||
|
|-------------------|--------|
|
||||||
|
| Duration 1 s, then Duration 2 s | open reply, **Duration 2 s** |
|
||||||
|
| Duration 2 s, then Duration 1 s | open reply, **Duration 1 s** |
|
||||||
|
| Clock 1, then Clock 3 | open reply, **Clock 3** |
|
||||||
|
| Clock 3, then Clock 4 (out of range) | **no reply** |
|
||||||
|
| Duration 1 s, then Duration 0 (invalid) | **no reply** |
|
||||||
|
| Duration 0 (invalid), then Duration 1 s | **no reply** |
|
||||||
|
| ServerFill `rand`, then a 33-character string (invalid) | **no reply** |
|
||||||
|
|
||||||
|
Among valid duplicates, the **last** occurrence wins for every known tag tested:
|
||||||
|
ProtocolVersion, Duration, Interval, Length, ReceivedStats, StampAt, Clock, DSCP
|
||||||
|
and ServerFill. Duration was additionally tested with an invalid occurrence in
|
||||||
|
both orders, and both requests were dropped. Clock and ServerFill were tested
|
||||||
|
with an invalid second occurrence, which was also dropped. The untested invalid
|
||||||
|
orders and parameters are not generalized here.
|
||||||
|
|
||||||
|
### 4.7 Absent Clock with a non-none restricted StampAt — upstream-defect vector
|
||||||
|
|
||||||
|
Capture: `../captures/server-clock-absent.pcapng`. Default-configuration server.
|
||||||
|
This vector records an **upstream robustness defect** (server specification
|
||||||
|
Section 22.5) when timestamp restriction leaves the requested non-none StampAt
|
||||||
|
non-none. It is a must-not-crash recommendation for a clean implementation, not
|
||||||
|
a behavior to reproduce.
|
||||||
|
|
||||||
|
| frame | direction | bytes | note |
|
||||||
|
|-------|-----------|-------|------|
|
||||||
|
| 1 | C → S | `14a75b01 05 06 06 06 04 00` | open; ReceivedStats = 3, StampAt = 3 (both), Length = 0, **no Clock tag** |
|
||||||
|
| 2 | S → C | `14a75b03 7ca44127b2cd7eab 01 02 05 06 06 06` | open reply, non-zero token, ProtocolVersion 1, ReceivedStats 3, StampAt 3 — **no Clock parameter** |
|
||||||
|
| 3 | C → S | `14a75b00 7ca44127b2cd7eab 00000000` | first echo, sequence 0 |
|
||||||
|
|
||||||
|
Frame 3 is the last packet in the capture. It receives no reply, and the server
|
||||||
|
process terminates; every other session on that process, on either address
|
||||||
|
family, stops being served at the same moment.
|
||||||
|
|
||||||
|
| variation | result |
|
||||||
|
|-----------|--------|
|
||||||
|
| Requested StampAt 1 (send), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
|
||||||
|
| Requested StampAt 2 (receive), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
|
||||||
|
| Requested StampAt 3 (both), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
|
||||||
|
| Requested StampAt 4 (midpoint), no Clock; restricted StampAt non-none | open accepted, first echo fatal |
|
||||||
|
| Requested StampAt 0 (none), no Clock | open accepted, session serves normally with no timestamps |
|
||||||
|
| **StampAt 3, explicit `Clock = 0`** | **open dropped, no reply, no session** — see Section 4.2 |
|
||||||
|
| Requested StampAt 1 / 2 / 3 / 4, no Clock; no-timestamp policy restricts StampAt to none | open accepted, session serves normally with no timestamps |
|
||||||
|
| Requested StampAt 1 / 2 / 3 / 4, no Clock; single-timestamp policy leaves restricted StampAt non-none | open accepted, first echo fatal |
|
||||||
|
|
||||||
|
The last two rows are scoped to requested non-none, absent-Clock cases only:
|
||||||
|
fatality depends on the server's restricted (effective) StampAt remaining
|
||||||
|
non-none. The `StampAt 0` row is served normally whatever the server's timestamp
|
||||||
|
policy, and the explicit `Clock = 0` row is rejected during negotiation before
|
||||||
|
any policy applies; neither becomes fatal under either configuration.
|
||||||
|
|
||||||
|
Reproduced on the 0.9.0 release, the 0.9.1 release and the tested post-0.9.1
|
||||||
|
development build.
|
||||||
|
|
||||||
|
The last four rows are the point of the vector: an omitted Clock and an explicit
|
||||||
|
`Clock = 0` are different inputs with opposite upstream outcomes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Reply Length
|
## 5. Reply Length
|
||||||
@@ -709,8 +819,10 @@ start the idle clock, wait, then two echoes back to back.
|
|||||||
| 7.5 s | answered | **dropped** |
|
| 7.5 s | answered | **dropped** |
|
||||||
| 9.0 s | answered | **dropped** |
|
| 9.0 s | answered | **dropped** |
|
||||||
|
|
||||||
Deadline = idle timeout + 5 s. The first request past the deadline is still
|
Observed session-release boundary = idle timeout + 5 s. The first
|
||||||
answered, and the session is released while handling it.
|
otherwise-serviceable request past that boundary is still answered once by
|
||||||
|
upstream, and the immediate follow-up is dropped. Section 8.3 shows what happens
|
||||||
|
when a packet that would be dropped anyway touches the token first.
|
||||||
|
|
||||||
**A session that has never carried an echo request does not expire.** With the
|
**A session that has never carried an echo request does not expire.** With the
|
||||||
same 2-second timeout, a session left completely idle for 9 seconds after open
|
same 2-second timeout, a session left completely idle for 9 seconds after open
|
||||||
@@ -719,6 +831,98 @@ was served normally on its first echo request.
|
|||||||
With the idle timeout disabled, a session idle for 9 seconds was served normally
|
With the idle timeout disabled, a session idle for 9 seconds was served normally
|
||||||
and remained usable.
|
and remained usable.
|
||||||
|
|
||||||
|
### 8.1 The grace is additive across configured timeouts
|
||||||
|
|
||||||
|
Session-release boundary sweep, probes spaced 50 ms, three repetitions per row,
|
||||||
|
gaps measured from the last accepted echo. Each probe used a first
|
||||||
|
otherwise-serviceable echo after the gap and an immediate follow-up; the boundary
|
||||||
|
is inferred from whether that follow-up was dropped:
|
||||||
|
|
||||||
|
| configured idle timeout | longest gap with follow-up served | shortest gap with follow-up dropped |
|
||||||
|
|-------------------------|----------------------------------|------------------------------------|
|
||||||
|
| 500 ms | 5.45 s | 5.50 s |
|
||||||
|
| 2 s | 6.95 s | 7.00 s |
|
||||||
|
| 4 s | 8.95 s | 9.00 s |
|
||||||
|
|
||||||
|
Every row sits at timeout + 5 s to within the probe spacing. At and beyond the
|
||||||
|
observed release boundary, the first otherwise-serviceable post-gap echo may
|
||||||
|
still receive a final lazy-release reply; the immediate follow-up is then
|
||||||
|
dropped. The margin is the same five seconds at each timeout, so it is additive
|
||||||
|
rather than proportional, and the boundary did not move with when the session
|
||||||
|
was opened. Reproduced on the 0.9.0 release, the 0.9.1 release and the tested
|
||||||
|
post-0.9.1 development build.
|
||||||
|
|
||||||
|
### 8.2 Which drop classes keep a session alive
|
||||||
|
|
||||||
|
Per row: open a session, send one echo (count 1), inject the listed packet, then
|
||||||
|
send another echo. The second echo's count shows whether the injected packet was
|
||||||
|
counted; separate timing trials show whether it moved the expiry boundary.
|
||||||
|
|
||||||
|
| injected packet | counted / window advanced | extends the idle lifetime |
|
||||||
|
|-----------------|---------------------------|---------------------------|
|
||||||
|
| bad magic | no | no |
|
||||||
|
| invalid flag bits | no | no |
|
||||||
|
| Reply or Open flag on a data packet | no | no |
|
||||||
|
| unknown token, or a zero token | no | no |
|
||||||
|
| valid token from a foreign source endpoint | no | no — but see 8.3 |
|
||||||
|
| echo truncated before the sequence number | no | no |
|
||||||
|
| request over the configured maximum length | no | no |
|
||||||
|
| bad, missing or zeroed MAC | no | no |
|
||||||
|
| **request with no rate allowance left** | **no** | **yes** |
|
||||||
|
|
||||||
|
The rate-limit row was separated from the alternatives three ways: a session fed
|
||||||
|
only too-fast requests outlived its idle deadline; the same cadence of oversized
|
||||||
|
requests did not; and a silent session expired on schedule. Reproduced on all
|
||||||
|
three builds. It is the only tested drop class that keeps a session alive.
|
||||||
|
|
||||||
|
### 8.3 The first packet to release an expired session determines whether a final reply is emitted
|
||||||
|
|
||||||
|
Capture: `../captures/server-expiry-consumption.pcapng`. Server idle timeout 3 s,
|
||||||
|
so the effective boundary is 8 s after the last accepted echo. Both trials are in
|
||||||
|
the one capture, which is why it is worth keeping: the control and the test differ
|
||||||
|
by a single injected packet.
|
||||||
|
|
||||||
|
Control — session on port 49865, token `fff56ec42eb8caaa`:
|
||||||
|
|
||||||
|
| frame | t | direction | packet | observed |
|
||||||
|
|-------|---|-----------|--------|----------|
|
||||||
|
| 3 | 0.00 s | C → S | echo seq 0 | reply, count 1 |
|
||||||
|
| 5 | 8.00 s | C → S | echo seq 1 — first past the deadline | **reply**, count 2 |
|
||||||
|
| 7 | 8.00 s | C → S | echo seq 2 | **no reply** — session released |
|
||||||
|
|
||||||
|
Test — session on port 53237, token `9e27590dbb016eb9`, opened at 8.70 s:
|
||||||
|
|
||||||
|
| frame | t | direction | packet | observed |
|
||||||
|
|-------|---|-----------|--------|----------|
|
||||||
|
| 10 | 8.70 s | C → S | echo seq 0 | reply, count 1 |
|
||||||
|
| 12 | 16.71 s | **X → S** | echo seq 9, correct token, **foreign source port 64688** | no reply — foreign endpoint, as at any time |
|
||||||
|
| 13 | 17.02 s | C → S | echo seq 1 — the client's first past the deadline | **no reply** |
|
||||||
|
| 14 | 17.72 s | C → S | echo seq 2 | no reply |
|
||||||
|
|
||||||
|
Frame 13 is the result. In the control the equivalent request was answered; here
|
||||||
|
the foreign-endpoint packet at frame 12 arrived 0.3 s earlier, just past the same
|
||||||
|
deadline, and the session was already gone. The foreign packet was itself dropped
|
||||||
|
and released the session without receiving a reply, so no later request received a
|
||||||
|
final reply. Whether it advanced the received count first cannot be read from this
|
||||||
|
trial — the session emitted no further reply, so no statistic reports it. The
|
||||||
|
not-counted result of Section 2 was measured on a live session, where a later
|
||||||
|
reply exposed the count, and does not carry over to this path.
|
||||||
|
|
||||||
|
The same effect was produced by a truncated echo, an oversized echo, a close
|
||||||
|
request from a foreign endpoint, and a burst of unrelated ordinary opens arriving
|
||||||
|
before the legitimate client's post-deadline request. These are the classes that
|
||||||
|
were tested; the effect is not claimed for every possible packet.
|
||||||
|
|
||||||
|
| injected before the client's post-deadline request | client's post-deadline request |
|
||||||
|
|----------------------------------------------------|--------------------------------|
|
||||||
|
| nothing (control) | **answered**, then the next one dropped |
|
||||||
|
| echo from a foreign source endpoint | **unanswered** |
|
||||||
|
| truncated echo | **unanswered** |
|
||||||
|
| oversized echo | **unanswered** |
|
||||||
|
| close from a foreign source endpoint | **unanswered** |
|
||||||
|
| burst of unrelated ordinary opens | **unanswered** |
|
||||||
|
| burst of unrelated **no-test** opens | **answered** — no-test opens create no session and reclaim none |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Malformed Packet Admission
|
## 9. Malformed Packet Admission
|
||||||
@@ -768,15 +972,74 @@ therefore 184 (0xb8) in this table, not 46.
|
|||||||
| 46 | 46 | delivered |
|
| 46 | 46 | delivered |
|
||||||
| 184 | 184 | delivered |
|
| 184 | 184 | delivered |
|
||||||
| 255 | 255 | delivered |
|
| 255 | 255 | delivered |
|
||||||
| 256 | 256 | **all dropped** |
|
| 256 | 256 | **no ECHO reply observed** |
|
||||||
| 300 | 300 | **all dropped** |
|
| 300 | 300 | **no ECHO reply observed** |
|
||||||
| −5 | −5 | delivered |
|
| −5 | −5 | delivered |
|
||||||
|
|
||||||
With DSCP disallowed by server policy, every requested value is returned as
|
With DSCP disallowed by server policy, every requested value is returned as
|
||||||
absent (0) and replies are delivered normally.
|
absent (0) and replies are delivered normally.
|
||||||
|
|
||||||
A value the server cannot apply to its socket is still accepted during
|
An out-of-range value is still accepted during negotiation; for values of 256 or
|
||||||
negotiation; the session then opens successfully and answers nothing.
|
above the session opens successfully but no ECHO reply was observed. Only that
|
||||||
|
session is affected — the server kept serving its other sessions throughout.
|
||||||
|
|
||||||
|
### 10.1 Interleaved sessions do not leak marks
|
||||||
|
|
||||||
|
Capture: `../captures/server-dscp-interleaved.pcapng`. Four sessions opened
|
||||||
|
back to back on one listener, then driven in three interleaved rounds of one echo
|
||||||
|
each. The TOS byte column is the value read from the reply's IP header.
|
||||||
|
|
||||||
|
| session (source port) | requested DSCP | open reply TOS | echo reply TOS, rounds 1–3 |
|
||||||
|
|-----------------------|----------------|----------------|-----------------------------|
|
||||||
|
| 62913 | 46 | **0x00** | 0x2e, 0x2e, 0x2e |
|
||||||
|
| 52559 | 8 | **0x00** | 0x08, 0x08, 0x08 |
|
||||||
|
| 57975 | not requested | **0x00** | 0x00, 0x00, 0x00 |
|
||||||
|
| 64688 | 184 | **0x00** | 0xb8, 0xb8, 0xb8 |
|
||||||
|
|
||||||
|
For each session in this capture, whose negotiated value is in `0..=255`, every
|
||||||
|
echo reply carries its own session's value verbatim; no reply carried another
|
||||||
|
session's value in any round. All four open replies are unmarked.
|
||||||
|
|
||||||
|
Note what this capture does **not** show: its four open exchanges are frames 1–8
|
||||||
|
and its first marked echo reply is frame 10, so no open reply here follows a
|
||||||
|
marked reply. For that ordering see Section 10.2.
|
||||||
|
|
||||||
|
### 10.2 Marking of the other packet kinds
|
||||||
|
|
||||||
|
| packet | marking |
|
||||||
|
|--------|---------|
|
||||||
|
| open reply | always TOS 0, whatever the session negotiated |
|
||||||
|
| echo reply | the session's negotiated value, verbatim for values in `0..=255` (see Section 10.3 for other values) |
|
||||||
|
| server-initiated close reply (Section 3.2) | measured to carry the same session marking for values in `0..=255`; no capture in this set shows it — the close-flagged reply in `../captures/server-close.pcapng` is from a session that negotiated no DSCP, and is TOS 0 |
|
||||||
|
| client-initiated close | no reply is sent at all |
|
||||||
|
|
||||||
|
An open reply is unmarked even on a listener that has already sent marked
|
||||||
|
replies. From `../captures/dscp-values.pcapng`, one listener serving two
|
||||||
|
sessions in sequence:
|
||||||
|
|
||||||
|
| frame | packet | TOS |
|
||||||
|
|-------|--------|-----|
|
||||||
|
| 3 | echo request, session 1 (DSCP 8) | `0x08` |
|
||||||
|
| 4 | echo reply, session 1 | `0x08` |
|
||||||
|
| 5 | close, session 1 | `0x00` |
|
||||||
|
| 6 | open request, session 2 | `0x00` |
|
||||||
|
| **7** | **open reply, session 2** | **`0x00`** |
|
||||||
|
| 8 | echo request, session 2 (DSCP 32) | `0x20` |
|
||||||
|
| 9 | echo reply, session 2 | `0x20` |
|
||||||
|
|
||||||
|
Frame 7 is the point: it follows the marked reply of frame 4 on the same
|
||||||
|
listener and is still unmarked.
|
||||||
|
|
||||||
|
### 10.3 Observed handling of negative and out-of-range DSCP values
|
||||||
|
|
||||||
|
| requested DSCP | address family | observed |
|
||||||
|
|----------------|----------------|----------|
|
||||||
|
| −1 | IPv4 | wire TOS **255** — the only negative value whose emitted marking was recorded; the transformation is not identified |
|
||||||
|
| −1 | IPv6 | traffic class **0** |
|
||||||
|
| 256 and above | either | negotiated and echoed back; no ECHO reply observed for that session |
|
||||||
|
|
||||||
|
The v4/v6 difference was observed on the tested host, not established as a wire
|
||||||
|
rule, and a compatible server is under no obligation to reproduce it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -788,6 +1051,9 @@ negotiation; the session then opens successfully and answers nothing.
|
|||||||
| `../captures/server-close-lifecycle.pcapng` | Client-initiated close, repeated close, and an echo after close — none answered |
|
| `../captures/server-close-lifecycle.pcapng` | Client-initiated close, repeated close, and an echo after close — none answered |
|
||||||
| `../captures/server-close.pcapng` | Server-initiated close via the maximum-duration limit; frame 36 carries flags `0x06` |
|
| `../captures/server-close.pcapng` | Server-initiated close via the maximum-duration limit; frame 36 carries flags `0x06` |
|
||||||
| `../captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, and the original endpoint continuing to be served |
|
| `../captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, and the original endpoint continuing to be served |
|
||||||
|
| `../captures/server-clock-absent.pcapng` | An open selecting timestamps but omitting the Clock tag is answered with a token and no Clock parameter; the first echo is the last packet in the exchange (Section 4.7) |
|
||||||
|
| `../captures/server-expiry-consumption.pcapng` | Control versus foreign-endpoint-first: whether the packet that releases an expired session emits a final reply (Section 8.3) |
|
||||||
|
| `../captures/server-dscp-interleaved.pcapng` | Four interleaved sessions with distinct DSCP values, three rounds each, no cross-session leakage, unmarked open replies (Section 10.1) |
|
||||||
|
|
||||||
All captures are loopback traffic with the server on a fixed port and the client
|
All captures are loopback traffic with the server on a fixed port and the client
|
||||||
on an ephemeral port.
|
on an ephemeral port.
|
||||||
@@ -799,3 +1065,7 @@ no capture filter string, and no filesystem path — only the packet records and
|
|||||||
the rewriting tool's own version string. Packet bytes and timestamps were **not**
|
the rewriting tool's own version string. Packet bytes and timestamps were **not**
|
||||||
altered: for each file, the full frame hexdump and the per-frame epoch timestamps
|
altered: for each file, the full frame hexdump and the per-frame epoch timestamps
|
||||||
were verified byte-identical before and after the rewrite.
|
were verified byte-identical before and after the rewrite.
|
||||||
|
|
||||||
|
The three captures added on 2026-08-12 were rewritten and verified the same way,
|
||||||
|
and their packet counts are 3, 14 and 32 respectively, unchanged from the
|
||||||
|
originals.
|
||||||
Reference in new issue
Block a user