1930 lines
94 KiB
Markdown
1930 lines
94 KiB
Markdown
# IRTT-Compatible Server Protocol Specification
|
||
|
||
**Version:** 1.1-verified
|
||
**Date:** 2026-08-11
|
||
**Compatibility baseline:** IRTT 0.9.1, protocol version 1
|
||
**Verification platform:** macOS Darwin 25.5.0 arm64
|
||
|
||
---
|
||
|
||
## 1. Status
|
||
|
||
This document describes the externally observable behavior required for an
|
||
independent **server** implementation that interoperates with existing IRTT
|
||
(Isochronous Round-Trip Tester) clients speaking protocol version 1.
|
||
|
||
It is the server-side counterpart to `IRTT_CLIENT_PROTOCOL_SPEC.md`. The two
|
||
documents share a wire format; this one adds only what a server must do that a
|
||
client does not. Where a rule is already stated in the client specification
|
||
(magic bytes, flag bits, field ordering, parameter encoding, HMAC computation),
|
||
this document references it rather than restating it, and states only the
|
||
server-specific reading of it.
|
||
|
||
This document does **not** describe upstream source structure, internal
|
||
algorithms, module layout, or implementation organization. It is intended for a
|
||
clean-room implementation that has never seen the upstream source code.
|
||
|
||
The key words "MUST", "MUST NOT", "REQUIRED", "SHOULD", "SHOULD NOT",
|
||
"RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as interoperability
|
||
requirements, consistent with RFC 2119.
|
||
|
||
Five distinct levels of statement are used throughout, and they are not
|
||
interchangeable. Nothing in one level implies anything at another.
|
||
|
||
| Marker | Meaning |
|
||
|--------|---------|
|
||
| **Protocol requirement** | Required for interoperability with existing clients. Stated with MUST / MUST NOT / SHOULD. Every one rests on observed behavior. |
|
||
| **Upstream 0.9.1 behavior** | What the reference server was observed to do on the wire. A compatible implementation is **not** obliged to reproduce it unless a separate protocol requirement says so. |
|
||
| **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. |
|
||
| **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. |
|
||
|
||
Where the reference server's own behavior would be a poor choice for a new
|
||
implementation — an unbounded resource policy, a fatal reaction to a single
|
||
failed send — this document records the observation and the recommendation
|
||
separately, and does not turn either into a protocol rule.
|
||
|
||
---
|
||
|
||
## 2. Scope and Compatibility Baseline
|
||
|
||
- **Server-only.** Client behavior appears only where a server's obligations
|
||
depend on it.
|
||
- **Protocol version 1**, as implemented by IRTT 0.9.0 and 0.9.1.
|
||
- The baseline for every unqualified statement is the **upstream 0.9.1
|
||
release**.
|
||
- Two other upstream builds were exercised, and each is named explicitly
|
||
wherever its results are used:
|
||
- the **0.9.0 release**, for the version comparison in Sections 11.3.1 and
|
||
21.1;
|
||
- a build of the **upstream development tree as it stood six commits past the
|
||
0.9.1 tag**, covered separately in Section 21.2. Those six commits are
|
||
*not* part of the 0.9.1 release, and no result measured against them
|
||
redefines the 0.9.1 baseline.
|
||
- Development-era (0.1.x) protocol variants are out of scope and are not
|
||
interoperable.
|
||
- CLI design, logging, output formats, configuration file syntax, and process
|
||
supervision are out of scope.
|
||
|
||
Implementations are not required to use any specific internal architecture,
|
||
data structure, naming convention, concurrency model, threading or task model,
|
||
socket ownership arrangement, or source organization. This document deliberately
|
||
prescribes none of those: they are decisions for the implementing project, not
|
||
protocol facts. Only externally observable behavior is normative.
|
||
|
||
---
|
||
|
||
## 3. Clean-Room Provenance
|
||
|
||
This specification was produced on the contaminated side of the clean-room
|
||
boundary described in `../clean-room/CLEANROOM_NOTES.md`. It records only
|
||
externally observable behavior:
|
||
|
||
- packet bytes on the wire, in both directions;
|
||
- accept / drop / reply decisions;
|
||
- session state transitions inferred from subsequent packet exchanges;
|
||
- negotiated parameter values returned to the client;
|
||
- timing boundaries measured by observation;
|
||
- documented command-line policy knobs and their observable effects.
|
||
|
||
Every normative statement in this document is supported by a black-box
|
||
experiment against a running reference server, by a packet capture in
|
||
`captures/`, or by published end-user documentation. Statements that could only
|
||
be derived from internal structure have been excluded.
|
||
|
||
**One labelled exception.** Section 11.3.1 carries a single sentence of
|
||
*historical* judgement — that a particular measured difference between 0.9.0 and
|
||
0.9.1 is an unintended regression rather than a designed feature — which was
|
||
reached with contaminated-side access to upstream material. It is labelled where
|
||
it appears, it is not normative, and no requirement anywhere in this document
|
||
depends on it. The measured 0.9.0-versus-0.9.1 wire difference is stated
|
||
separately and stands on its own. The contaminated-side evidence behind the
|
||
judgement was retained on the contaminated side and deliberately not carried
|
||
into this document; see `../clean-room/CLEANROOM_NOTES.md`.
|
||
|
||
**Naming.** The parameter names used throughout (ProtocolVersion, Duration,
|
||
Interval, Length, ReceivedStats, StampAt, Clock, DSCP, ServerFill) are taken
|
||
from upstream's **published** documentation of its client's machine-readable
|
||
output, not from source inspection. No other upstream identifier appears in this
|
||
document.
|
||
|
||
---
|
||
|
||
## 4. Roles and Directionality
|
||
|
||
A server:
|
||
|
||
1. listens on one or more UDP endpoints;
|
||
2. accepts session-open requests and assigns session tokens;
|
||
3. echoes test packets, annotating them with reception statistics and
|
||
timestamps as negotiated;
|
||
4. releases session state on client request, on policy limits, or on idle
|
||
expiry.
|
||
|
||
Every packet a server emits **MUST** have the Reply flag (0x02) set. Every
|
||
packet a server accepts **MUST** have the Reply flag clear.
|
||
|
||
**Protocol requirement.** A reply without the Reply flag is not merely ignored
|
||
by existing clients — upstream 0.9.1 clients abort the run and terminate
|
||
abnormally when a reply arrives with the flag clear. This was confirmed by
|
||
driving an upstream client against a deliberately non-conforming server.
|
||
|
||
---
|
||
|
||
## 5. Transport and Packet Envelope
|
||
|
||
### 5.1 Transport
|
||
|
||
UDP only. There is no TCP fallback, no connection setup at the transport layer,
|
||
and no fragmentation logic in the protocol itself.
|
||
|
||
The conventional server port is **2112** (server policy: the reference server's
|
||
default bind is all addresses on port 2112).
|
||
|
||
### 5.2 Address Families
|
||
|
||
A server MUST support IPv4. IPv6 support is RECOMMENDED and is provided by the
|
||
reference server.
|
||
|
||
**Upstream 0.9.1 behavior.** Binding to an unspecified address creates two
|
||
independent listeners, one per address family, on the same port. Tokens are
|
||
scoped to the listener that issued them: a token issued by the IPv4 listener is
|
||
not recognized by the IPv6 listener on the same port, and a request bearing it
|
||
is dropped as an unknown token. See `captures/server-session-identity.pcapng` and
|
||
the vectors file for the equivalent same-family case.
|
||
|
||
**Protocol requirement.** Session tokens need only be unique within the scope in
|
||
which they are looked up. A compatible implementation MAY use one global table
|
||
or one per listener; clients never observe the difference except through the
|
||
drop behavior above.
|
||
|
||
### 5.3 Reply Addressing
|
||
|
||
**Protocol requirement.** A reply MUST be sent to the exact source address and
|
||
port of the request, and MUST be sent **from** the address and port to which the
|
||
request was addressed. Clients use connected UDP sockets and silently discard
|
||
datagrams whose source does not match the address they sent to.
|
||
|
||
**Server policy.** On a multi-homed host with a listener bound to an unspecified
|
||
address, choosing the reply source address by routing table rather than by the
|
||
request's destination address will break clients on the non-preferred address.
|
||
The reference server offers an opt-in policy to pin the reply source address to
|
||
the request's destination address, and its documentation recommends binding to
|
||
explicit addresses instead. On single-homed and loopback configurations both
|
||
modes were observed to produce correct source addresses.
|
||
|
||
### 5.4 Packet Admission
|
||
|
||
This section states which datagrams a server answers and which it discards. It
|
||
describes outcomes, not an internal processing pipeline: any implementation that
|
||
produces these accept/discard decisions is conformant, whatever order it
|
||
evaluates them in.
|
||
|
||
**Discard conditions.** Each of the following was independently observed to
|
||
cause the datagram to be **silently discarded**, with no reply of any kind and
|
||
no change to the state of any live session:
|
||
|
||
| Condition | Notes |
|
||
|-----------|-------|
|
||
| Datagram too short to carry magic + flags | With an HMAC key configured, the authentication field is part of the floor, so the floor rises by 16 bytes |
|
||
| First three bytes are not the protocol magic | |
|
||
| Any flag bit above the four defined bits is set (flags > 0x0F) | |
|
||
| The Reply flag is set | A server never answers a datagram carrying it |
|
||
| Authentication mismatch of any kind | Section 14; includes the flag/key mismatch in either direction |
|
||
| A non-open datagram too short to carry the token | |
|
||
| A non-open datagram whose token matches no live session | Section 8 |
|
||
| A non-open datagram whose token matches a session bound to a different source endpoint | Section 8; the session itself is unaffected |
|
||
| An echo datagram too short to carry the sequence number | |
|
||
| An echo datagram exceeding a configured maximum packet length | Section 9.4, server policy |
|
||
| An echo datagram arriving with no rate allowance left | Section 9.3, server policy |
|
||
| An open datagram whose parameter payload is malformed or out of range | Section 7.2 |
|
||
|
||
**Protocol requirement.** Every rejection is a **silent discard**. The protocol
|
||
defines no error reply, no reset, and no NACK. A client distinguishes "rejected"
|
||
from "lost" only by timing out. A compatible server MUST NOT answer any of the
|
||
conditions above.
|
||
|
||
**Observable precedence.** Only two ordering relationships are externally
|
||
visible, and both were verified directly. Both describe how an *admissible*
|
||
datagram is interpreted; neither weakens any discard condition in the table
|
||
above.
|
||
|
||
1. **For an otherwise admissible datagram with Open set, Open interpretation
|
||
takes precedence over Close interpretation and over an echo-shaped body.** A
|
||
datagram with both Open and Close set is a *no-test* open (Section 16), not a
|
||
close; and an echo-shaped datagram that also carries the Open flag is answered
|
||
as an open, with the bytes that would have been the token and sequence number
|
||
parsed as Open parameter data rather than as those fields. This precedence is
|
||
about interpretation only. It does **not** override the independent rejection
|
||
conditions above: a datagram carrying the Open flag is still silently
|
||
discarded if the Reply flag is also set, if any undefined flag bit is set, if
|
||
authentication fails, or if its Open parameter payload is malformed or out of
|
||
range. A compatible server SHOULD behave the same way, so that a client's
|
||
retransmitted open is never mistaken for an echo.
|
||
2. **An authentication failure is indistinguishable from an unknown token.** A
|
||
datagram carrying a *valid* token with a bad MAC produces exactly the same
|
||
silence as one carrying a token that was never issued (Section 14.2). A
|
||
compatible server MUST preserve this: no reply behavior may reveal whether a
|
||
token exists, for a datagram that failed authentication.
|
||
|
||
**Request bytes beyond the sequence number carry no meaning to a server.** The
|
||
midpoint/receive-send exclusivity and clock-consistency rules that govern
|
||
timestamp fields apply to *replies*; they have no effect on inbound traffic. An
|
||
echo request carrying arbitrary bytes in the region a reply would use for
|
||
timestamps is accepted and answered normally.
|
||
|
||
### 5.5 Minimum and Maximum Datagram Sizes
|
||
|
||
Observed minimum accepted request sizes (no authentication):
|
||
|
||
| Request kind | Minimum accepted datagram |
|
||
|--------------|---------------------------|
|
||
| Open (with empty parameter payload) | 4 bytes |
|
||
| Close | 12 bytes |
|
||
| Echo | 16 bytes |
|
||
|
||
With authentication enabled, add 16 bytes to each figure.
|
||
|
||
**Upstream 0.9.1 behavior.** Trailing bytes beyond the fields a request kind
|
||
requires are ignored:
|
||
|
||
- an echo request longer than the negotiated length is accepted and answered
|
||
with a reply of the *negotiated* length, not the request length;
|
||
- a close request with trailing bytes is accepted and closes the session.
|
||
|
||
The only length ceiling applied to requests is the configured maximum packet
|
||
length (Section 19).
|
||
|
||
---
|
||
|
||
## 6. Server Lifecycle
|
||
|
||
### 6.1 Startup
|
||
|
||
**Server policy.** The reference server resolves its bind specifications,
|
||
creates one listener per resolved address/family, and serves until interrupted.
|
||
A failure on any single listener causes the whole server to stop.
|
||
|
||
### 6.2 Shutdown
|
||
|
||
**Upstream 0.9.1 behavior.** Termination is abrupt from the client's
|
||
perspective. No close, reset, or notification packet of any kind is emitted for
|
||
live sessions when the server shuts down; subsequent client packets simply go
|
||
unanswered. Verified by signalling a running server mid-session and observing
|
||
that nothing arrived at the client before the socket went silent.
|
||
|
||
**Protocol requirement.** None. Clients MUST already tolerate an unresponsive
|
||
server. A compatible implementation MAY emit close replies to live sessions on
|
||
shutdown; upstream clients ignore unsolicited close replies harmlessly
|
||
(Section 15.1).
|
||
|
||
### 6.3 Reply Send Failures
|
||
|
||
**Upstream 0.9.1 behavior.** If sending a reply fails with a non-transient
|
||
socket error, the listener terminates and the whole server process exits. This
|
||
is reachable in ordinary use: a client may negotiate a packet length larger than
|
||
the host's maximum outbound datagram size, and the first echo reply then ends the
|
||
server. See Section 22.
|
||
|
||
**Protocol requirement:** none. A reply that is never sent is
|
||
indistinguishable, to the client, from a reply that was lost in the network. No
|
||
client can observe how the server reacted internally.
|
||
|
||
**Robustness recommendation (clean implementation).** Because the outcome above
|
||
is reachable from ordinary client traffic and takes every other session down
|
||
with it, a clean server should treat a per-packet send failure as a per-packet
|
||
event: drop that reply, keep the listener and the process running, and continue
|
||
serving other sessions. This is a resilience choice for the implementing
|
||
project, not something compatibility requires.
|
||
|
||
---
|
||
|
||
## 7. OPEN Negotiation
|
||
|
||
### 7.1 Open Request
|
||
|
||
**Direction:** client → server.
|
||
|
||
**Flags:** Open (0x01) set, Reply (0x02) clear, HMAC (0x08) set iff the client
|
||
is authenticating, Close (0x04) set iff the client requests *no-test*
|
||
(Section 16).
|
||
|
||
**Layout:** magic (3) + flags (1) + [authentication (16)] + parameter payload.
|
||
|
||
**Protocol requirement.** There is **no** token field in an open request. The
|
||
parameter payload begins immediately after the fixed header (and the
|
||
authentication field, when present).
|
||
|
||
The parameter payload uses the tag/value encoding defined in the client
|
||
specification, Section 8.6. A server MUST decode it with the same rules.
|
||
|
||
### 7.2 Parameter Acceptance and Rejection
|
||
|
||
A server parses the whole parameter payload. Observed acceptance rules:
|
||
|
||
| Condition | Observed result |
|
||
|-----------|-----------------|
|
||
| Empty parameter payload | Accepted; all parameters default to zero, then restriction applies |
|
||
| Unknown tag with a well-formed value | Silently ignored; the rest of the payload is still parsed |
|
||
| Truncated tag or truncated value varint | **Dropped, no reply** |
|
||
| Varint overflow | **Dropped, no reply** |
|
||
| Duration present and ≤ 0 | **Dropped, no reply** |
|
||
| Interval present and ≤ 0 | **Dropped, no reply** |
|
||
| Duration or Interval absent (implicitly 0) | Accepted |
|
||
| ReceivedStats outside 0–3 | **Dropped, no reply** |
|
||
| StampAt outside 0–4 | **Dropped, no reply** |
|
||
| Clock outside 1–3 | **Dropped, no reply** |
|
||
| Length any value, including negative | Accepted (see Section 7.3) |
|
||
| DSCP any value, including negative or > 255 | Accepted (see Section 7.3) |
|
||
| ServerFill string longer than 32 bytes | **Dropped, no reply** |
|
||
| ServerFill declared length exceeding the remaining payload | **Dropped, no reply** |
|
||
| Parameter payload far larger than any real request (200+ bytes of unknown tags tested) | Accepted |
|
||
|
||
**Protocol requirement.** Unknown tags MUST be silently ignored. A malformed
|
||
parameter payload MUST NOT produce a reply — a rejection reply would let an
|
||
unauthenticated peer distinguish a live server from a firewall drop, which is
|
||
contrary to the design of the authenticated mode.
|
||
|
||
**Clarification.** Upstream servers impose no 128-byte ceiling on *incoming*
|
||
parameter payloads. A payload of over 200 bytes composed of unknown tags was
|
||
accepted and answered normally. Any such limit in an implementation is a local
|
||
buffer-sizing choice, not a protocol rule.
|
||
|
||
### 7.3 Server Restriction of Parameters
|
||
|
||
A server MAY reduce or replace requested parameters. The restricted values are
|
||
what it returns in the open reply and what it enforces thereafter.
|
||
|
||
Observed restriction rules of upstream 0.9.1 (each is **server policy**, gated
|
||
on the corresponding configuration knob):
|
||
|
||
| Parameter | Restriction applied |
|
||
|-----------|--------------------|
|
||
| ProtocolVersion | Always rewritten to 1 (see Section 7.5) |
|
||
| Duration | Clamped down to the configured maximum test duration, when one is set |
|
||
| Interval | Raised to the configured minimum interval, when one is set |
|
||
| Interval | Then clamped down to (idle timeout ÷ 4), when an idle timeout is set |
|
||
| Length | Clamped down to the configured maximum packet length, when one is set |
|
||
| StampAt | Reduced per the configured timestamp allowance (Section 11.4) |
|
||
| DSCP | Forced to 0 when DSCP is disallowed or unsupported on the socket |
|
||
| ServerFill | Replaced with the server's own default fill descriptor when the requested fill does not match the allow-list |
|
||
|
||
Measured confirmations of the interval cap: idle timeout 4 s → maximum interval
|
||
1 s; 8 s → 2 s; 20 s → 5 s; 40 s → 10 s; 60 s → 15 s.
|
||
|
||
**Interoperability note.** The interval cap can *reduce* an interval below what
|
||
the client asked for. Upstream clients enforce a hard floor of **1 second** on a
|
||
server-reduced interval and abort the session — in both strict and loose mode —
|
||
when the returned interval is below it. A reduction to exactly 1 s is accepted
|
||
in loose mode. A server whose idle timeout is below 4 s will therefore be unable
|
||
to serve clients requesting a 1 s interval. Measured: idle timeout 2 s reduces
|
||
1 s → 500 ms and the client aborts; idle timeout 3 s reduces to 750 ms and the
|
||
client aborts; idle timeout 4 s produces no reduction at all for a 1 s request.
|
||
|
||
**Server-policy hazard.** If the configured minimum interval is larger than
|
||
(idle timeout ÷ 4), the negotiated interval is the smaller of the two while 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
|
||
minimum interval and an 8 s idle timeout: negotiated interval 2 s, but only the
|
||
first request in each 5 s window was answered.
|
||
|
||
### 7.4 Open Reply
|
||
|
||
**Direction:** server → client.
|
||
|
||
**Flags:** Open (0x01) set, Reply (0x02) set, HMAC (0x08) set iff the server is
|
||
authenticating, Close (0x04) set iff the request had it set (no-test) — see
|
||
Section 7.5 for the one case where upstream does *not* set it.
|
||
|
||
**Layout:** magic (3) + flags (1) + [authentication (16)] + token (8) +
|
||
restricted parameter payload.
|
||
|
||
**Protocol requirement.**
|
||
|
||
- The token field MUST be present in an open reply.
|
||
- For a session-creating open, the token MUST be non-zero. Clients treat a zero
|
||
token without the Close flag as an error.
|
||
- The parameter payload MUST contain the **restricted** values — the values the
|
||
server will actually enforce. Clients compare them field-by-field against
|
||
what they requested and, by default, abort on any difference.
|
||
- Parameters whose restricted value is zero are omitted from the payload
|
||
entirely; the client reconstructs them as zero.
|
||
- The open reply is **not** padded to the negotiated length. Its size is
|
||
header + token + parameter payload.
|
||
|
||
**Upstream 0.9.1 behavior.** The open reply preserves whatever flags the request
|
||
carried, adding Reply. There is no separate "reject" reply shape.
|
||
|
||
### 7.5 Protocol Version — Important Deviation
|
||
|
||
**Upstream 0.9.1 behavior.** The server does **not** reject a mismatched
|
||
protocol version. Any value in the ProtocolVersion parameter — 0, 2, −1, or the
|
||
tag omitted entirely — is silently rewritten to 1, a normal session is created,
|
||
and the open reply carries ProtocolVersion = 1 with the Close flag **clear**.
|
||
Verified by black-box test: requests with ProtocolVersion 0, 2 and −1 each
|
||
produced a session-creating open reply.
|
||
|
||
Consequence: version mismatch is detected **client-side only**, by the client
|
||
comparing the returned version against its own. A client speaking a future
|
||
version would send its own version, receive 1, and abort locally.
|
||
|
||
**Protocol requirement.** A compatible server MUST return ProtocolVersion = 1 in
|
||
the open reply so that version-1 clients accept the session. A compatible server
|
||
MAY additionally reject unknown versions by replying with the Close flag set and
|
||
a zero token; existing clients treat such a reply as a rejection and abort. Both
|
||
behaviors are interoperable with version-1 clients; only the second is
|
||
interoperable with a hypothetical future client that relies on server-side
|
||
detection.
|
||
|
||
**Do not assume** that an upstream server will tell you your version is wrong.
|
||
|
||
### 7.6 Repeated and Retransmitted Opens
|
||
|
||
**Upstream 0.9.1 behavior.** Every open request creates a new, independent
|
||
session with a fresh token. There is no deduplication of retransmitted opens and
|
||
no per-endpoint session limit:
|
||
|
||
- two opens from the *same* source port yield two distinct tokens, and **both**
|
||
remain usable simultaneously;
|
||
- an open request sent twice (as a client retransmission would) yields two
|
||
distinct tokens; the client keeps only the token from the reply it processes,
|
||
and the other session is orphaned;
|
||
- 1000 sequential opens from a single source endpoint all succeeded, all tokens
|
||
were distinct, and the first, middle and last were all still usable
|
||
afterwards.
|
||
|
||
**Resource consequence (upstream policy).** An orphaned session created by a
|
||
retransmitted open never carries an echo request, and — under upstream's idle
|
||
policy — a session that has never carried an echo request **never expires**
|
||
(Section 18.2). On a lossy path, open retransmissions therefore accumulate
|
||
permanent state.
|
||
|
||
**Protocol requirement:** none. Clients cannot observe orphaned sessions, and
|
||
nothing about the behavior above is required for interoperability.
|
||
|
||
**Robustness recommendation (clean implementation).** Bound the lifetime of
|
||
sessions that have never carried an echo request, so that open retransmissions
|
||
cannot accumulate. See Section 18.3.
|
||
|
||
---
|
||
|
||
## 8. Session Identity and Tokens
|
||
|
||
### 8.1 What Identifies a Session
|
||
|
||
**Protocol requirement.** A post-open request is bound to a session by the
|
||
**token**, and the server MUST additionally verify that it arrived from the
|
||
same source endpoint the session was opened from.
|
||
|
||
Observed identity components, all of which must match:
|
||
|
||
| Component | Must match |
|
||
|-----------|-----------|
|
||
| Token (64-bit, little-endian) | Yes |
|
||
| Source IP address | Yes |
|
||
| Source UDP port | Yes |
|
||
| IPv6 zone/scope, when present | Yes |
|
||
| Address family / listener | Yes — tokens are not shared between the IPv4 and IPv6 listeners of the same port |
|
||
|
||
Observed outcomes:
|
||
|
||
| Case | Result |
|
||
|------|--------|
|
||
| Correct token, original endpoint | Served normally |
|
||
| Correct token, **different source port**, same IP | **Dropped, no reply.** Session left intact and still usable from the original port |
|
||
| Correct token, different address family | **Dropped, no reply** (different listener, unknown token there) |
|
||
| Unknown token | **Dropped, no reply** |
|
||
| Zero token | **Dropped, no reply** (treated as any other unknown token) |
|
||
| Token of a session already closed | **Dropped, no reply** |
|
||
| **Close** request bearing a valid token from a foreign source port | **Dropped**; the session is **not** closed |
|
||
|
||
The last row matters: endpoint binding protects the close path as well as the
|
||
echo path. A capture of these cases is in
|
||
`captures/server-session-identity.pcapng`.
|
||
|
||
**Practical consequence.** Any NAT rebinding, source-port change, or address
|
||
change during a session terminates it from the client's point of view — all
|
||
subsequent packets go unanswered until the idle timeout reclaims the state.
|
||
There is no rebinding or migration mechanism in protocol version 1.
|
||
|
||
### 8.2 Token Semantics
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Width | 64 bits |
|
||
| Encoding | Little-endian in the token field |
|
||
| Assigned by | Server, during open |
|
||
| Zero token | Reserved in practice: clients reject a zero token in a session-creating open reply, and a zero token is used in no-test replies (Section 16) |
|
||
| Uniqueness | MUST be unique among the server's live sessions in the scope where lookups occur |
|
||
| Lifetime | From open until close, policy limit, or idle expiry |
|
||
| Reuse | A token that has been released MAY be issued again later; no client-visible barrier exists |
|
||
|
||
**Protocol requirement.** The server MUST echo the session token back in every
|
||
reply it sends for that session.
|
||
|
||
**Upstream 0.9.1 client behavior (informative).** Upstream clients do **not**
|
||
validate the token in echo replies — a reply carrying a wrong token was
|
||
accepted and recorded. Do not rely on this: other implementations may validate,
|
||
and matching the token is required by this specification.
|
||
|
||
**Token generation is deliberately unspecified.** A compatible implementation
|
||
MAY choose any generation strategy that satisfies the uniqueness requirement.
|
||
Because the token is the only thing standing between an off-path attacker and a
|
||
usable session on a non-authenticated server, generating tokens from a
|
||
cryptographically strong source is RECOMMENDED.
|
||
|
||
---
|
||
|
||
## 9. ECHO Processing
|
||
|
||
### 9.1 Echo Request
|
||
|
||
**Direction:** client → server.
|
||
|
||
**Flags:** Open clear, Reply clear, Close clear, HMAC set iff authenticating.
|
||
|
||
**Layout:** magic (3) + flags (1) + [authentication (16)] + token (8) +
|
||
sequence number (4) + client padding to the negotiated length.
|
||
|
||
**Protocol requirement.** A server MUST read only the token and the 32-bit
|
||
little-endian sequence number from a request. Everything after the sequence
|
||
number is opaque to the server: clients place zeroed placeholders there so the
|
||
request reaches the negotiated length, and a server MUST NOT interpret them.
|
||
|
||
**Upstream 0.9.1 behavior.** The datagram length of a request is not compared
|
||
against the negotiated length. A 16-byte request on a session that negotiated
|
||
64 bytes is accepted and answered with a 64-byte reply. A request *longer* than
|
||
the negotiated length is likewise accepted, and the reply is still the
|
||
negotiated length.
|
||
|
||
### 9.2 Echo Reply
|
||
|
||
**Direction:** server → client.
|
||
|
||
**Flags:** Reply (0x02) set; HMAC (0x08) set iff authenticating; Close (0x04)
|
||
set only in the server-initiated close case (Section 15.2). Open MUST be clear —
|
||
upstream clients abort if an echo reply has the Open flag set.
|
||
|
||
**Layout (fields in the order defined in the client specification,
|
||
Section 8.1.3):**
|
||
|
||
```
|
||
magic (3) | flags (1) | [auth (16)] | token (8) | sequence (4)
|
||
| [received count (4)] -- iff negotiated stats include "count"
|
||
| [received window (8)] -- iff negotiated stats include "window"
|
||
| [timestamp fields] -- per negotiated StampAt and Clock
|
||
| payload padding to the negotiated length
|
||
```
|
||
|
||
**Protocol requirement.**
|
||
|
||
- The sequence number MUST be copied from the request unchanged.
|
||
- The token MUST be copied from the request unchanged.
|
||
- The reply length MUST be at least the length of the fields it contains, and
|
||
SHOULD be exactly the negotiated length when that is larger.
|
||
- When the negotiated length is smaller than the field block (including the
|
||
common case of a negotiated length of 0), the reply is the field block's
|
||
length. Measured: a session negotiating length 0 or 20 with full statistics
|
||
and dual timestamps on both clocks produced 60-byte replies.
|
||
|
||
Writing `normal_header` for the block of fields the negotiated parameters imply:
|
||
|
||
```
|
||
compatible_reply_len = max(negotiated_length, normal_header)
|
||
```
|
||
|
||
For StampAt = Midpoint with a single negotiated clock, upstream 0.9.1 emits one
|
||
8-byte timestamp field more than the negotiated parameters imply, which changes
|
||
the reply length in exactly the cases where the field block wins. See
|
||
Section 11.3.1.
|
||
|
||
**Upstream 0.9.1 behavior — reply length is capped by the local interface MTU.**
|
||
The negotiated length is echoed back to the client unchanged, but the actual
|
||
reply datagram is truncated to the detected MTU of the listener's interface.
|
||
Measured on a 1500-byte-MTU interface: negotiated 1501, 2000, 3000 and 8000 all
|
||
produced 1500-byte replies, while the open reply still reported the requested
|
||
value. Upstream clients treat a reply shorter than the negotiated length as a
|
||
fatal error, so this configuration fails on the first echo. See Section 22.
|
||
|
||
### 9.3 Rate Limiting
|
||
|
||
**Server policy.** The reference server enforces the negotiated minimum send
|
||
interval with a token-bucket allowance:
|
||
|
||
- a burst allowance of *N* requests is available immediately (default N = 5);
|
||
- allowance is replenished at one request per configured minimum interval and
|
||
is capped at *N*;
|
||
- a request arriving with no allowance is **dropped without a reply**.
|
||
|
||
Measured with a 100 ms minimum interval and a burst of 5: a blast of 12 requests
|
||
produced exactly 5 replies; after a 600 ms pause, the next 3 spaced requests
|
||
were all answered. With a burst of 1, a blast of 4 produced 1 reply.
|
||
|
||
**Protocol requirement.** A rate-limited request MUST NOT be answered and MUST
|
||
NOT affect the reception statistics (Section 10). Measured: the received count
|
||
advanced 1, 2, 3, 4, 5 across the accepted burst and continued 6, 7, 8 for the
|
||
later requests — dropped requests were not counted.
|
||
|
||
### 9.4 Maximum Request Length
|
||
|
||
**Server policy.** When a maximum packet length is configured, a request whose
|
||
**datagram length** exceeds it is dropped without a reply. Measured with a
|
||
maximum of 64: 16-, 32- and 64-byte requests were answered with 64-byte replies;
|
||
65- and 100-byte requests were dropped.
|
||
|
||
The maximum applies to echo requests only. An open request larger than the
|
||
maximum was still accepted and answered.
|
||
|
||
If the configured maximum is below the minimum echo field block, **every** echo
|
||
request is dropped while opens still succeed. Measured with a maximum of 10: the
|
||
session opened, negotiated a length of 10, and every 16-byte echo request was
|
||
dropped.
|
||
|
||
---
|
||
|
||
## 10. Sequence Handling and Received Statistics
|
||
|
||
This section is the most behaviorally distinctive part of the server and is
|
||
specified strictly as input → observed output. The reference implementation's
|
||
observable semantics differ from a naive reading of "a 64-packet reception
|
||
bitmap", and clients depend on the observed behavior.
|
||
|
||
### 10.1 Received Count
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Field width | 32 bits, little-endian |
|
||
| Present when | Negotiated ReceivedStats includes "count" (values 1 or 3) |
|
||
| Initial value | 0 at session creation; the first accepted request is answered with 1 |
|
||
| Scope | Per session. A new session starts at 0 again |
|
||
| Counts | Every echo request that reaches the reply stage |
|
||
| Does **not** count | Requests dropped for bad magic, bad flags, bad authentication, unknown token, endpoint mismatch, oversize, or rate limiting |
|
||
| Duplicates | **Counted.** A repeated sequence number increments the count |
|
||
| Reordered / late requests | **Counted** |
|
||
| Close and open requests | Not counted |
|
||
|
||
**Protocol requirement.** The count reported in a reply MUST include the request
|
||
being answered.
|
||
|
||
**Wraparound.** The field is 32 bits. Behavior at 2^32 requests in one session
|
||
was not observed and is listed in Section 23.
|
||
|
||
### 10.2 Received Window
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Field width | 64 bits, little-endian |
|
||
| Present when | Negotiated ReceivedStats includes "window" (values 2 or 3) |
|
||
| Bit 0 (LSB) | The request being answered |
|
||
| Bit *k* | The request with sequence number (current − *k*), for 1 ≤ *k* ≤ 63 |
|
||
| Set bit | That sequence number was received by the server in this session |
|
||
| Initial value | 0 at session creation |
|
||
|
||
**Observed upstream 0.9.1 semantics.** The table below is a summary of the
|
||
measured input → output vectors in
|
||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1, expressed as a relation
|
||
between two values a client can see on the wire: the window in the previous
|
||
reply and the window in the next one. It is a description of observed output,
|
||
not a prescription of how to compute it — any method producing these values is
|
||
conformant.
|
||
|
||
Let `S` be the sequence number of the incoming request, `L` the sequence number
|
||
carried by the previous accepted request of the session, `W` the window value
|
||
carried by the previous reply (`W = 0` before the session's first request), and
|
||
|
||
```
|
||
Δ = (S − L) mod 2^32 -- unsigned 32-bit difference
|
||
```
|
||
|
||
Three cases account for every observed vector:
|
||
|
||
| Condition | Resulting window |
|
||
|-----------|------------------|
|
||
| Δ = 0 | `W \| 1` |
|
||
| 1 ≤ Δ ≤ 63 | `(W << Δ) \| 1` |
|
||
| Δ ≥ 64 | `0x1` |
|
||
|
||
Read through the cases a client actually encounters:
|
||
|
||
| Situation | Δ | Resulting window |
|
||
|-----------|---|------------------|
|
||
| First request of a session | any | `0x1` (because `W` is 0) |
|
||
| In order, no gap | 1 | `(W << 1) \| 1` |
|
||
| Duplicate of the most recent sequence number | 0 | `W`, unchanged |
|
||
| Gap ahead, within range | 2 … 63 | `(W << Δ) \| 1` |
|
||
| Gap ahead of 64 or more | ≥ 64 | **`0x1`** |
|
||
| **Out of order / late arrival** | large, because the unsigned difference wraps | **`0x1`** |
|
||
|
||
**The last two rows are the critical ones.** Because the difference is unsigned,
|
||
a sequence number *lower* than the previous one produces a very large Δ and
|
||
therefore falls into the third case. A late or reordered request does **not**
|
||
set a historical bit; it resets the window to `0x1`. The information that
|
||
earlier packets were received is lost, and it is not recovered by later packets
|
||
— the window rebuilds from scratch.
|
||
|
||
**Protocol requirement.** Bit 0 is always set in a window a server emits. A
|
||
compatible server MUST set bit 0. Consequently a window value of 0 is never
|
||
produced by upstream 0.9.1 when the window field is negotiated at all; clients
|
||
MAY continue to treat 0 defensively as "no information".
|
||
|
||
**Client-facing consequence, and why this must be stated.** A window of `0x1` is
|
||
a *valid* window that carries no information about earlier packets. It does
|
||
**not** mean the previous 63 sequence numbers were lost; it means no useful
|
||
prior received-history is represented in the window after the transition that
|
||
produced it. A client that reads `0x1` literally will classify up to 63 earlier
|
||
packets as lost upstream after a single reordering event or after any gap of 64
|
||
or more. Implementations of either side SHOULD treat `0x1` on a non-first packet
|
||
as "no historical information available" rather than "nothing earlier was
|
||
received".
|
||
|
||
This is a statement about what the window *represents*. How a client turns
|
||
window bits into an upstream/downstream loss classification is a separate
|
||
question, out of scope here, and is scheduled for its own audit on the clean
|
||
side; nothing in this section should be read as settling it.
|
||
|
||
**Sequence wraparound.** The comparison is modular over 32 bits, so a session
|
||
that crosses the u32 boundary continues normally. Measured with sequence numbers
|
||
4294967293, 4294967294, 4294967295, 0, 1: windows advanced
|
||
`0x1, 0x3, 0x7, 0xf, 0x1f` with counts 1…5, exactly as for a non-wrapping run.
|
||
|
||
**Sequence numbers need not start at 0.** A session whose first request carries
|
||
sequence 100 behaves identically to one starting at 0: window `0x1`, then `0x3`,
|
||
then `0x7`.
|
||
|
||
### 10.3 Behavioral Vectors
|
||
|
||
See `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` for the full input → output
|
||
tables, and `captures/server-recv-window.pcapng` for a capture of the
|
||
gap / duplicate / reorder / far-gap sequence.
|
||
|
||
---
|
||
|
||
## 11. Timestamp Semantics
|
||
|
||
### 11.1 What Each Field Means
|
||
|
||
| Negotiated StampAt | Fields emitted | Instant represented |
|
||
|--------------------|----------------|---------------------|
|
||
| None (0) | none | — |
|
||
| Send (1) | send wall and/or send monotonic | The server's departure instant for this reply |
|
||
| Receive (2) | receive wall and/or receive monotonic | The server's arrival instant for this request |
|
||
| Both (3) | receive and send fields | Both of the above |
|
||
| Midpoint (4) | midpoint fields | The arithmetic mean of the receive and send instants |
|
||
|
||
The externally relevant property is the *relationship* between the two instants,
|
||
not where in a server's internals they are taken. The pair must bracket the
|
||
server's own handling of the request as tightly as the implementation can manage,
|
||
so that the difference between them is a usable estimate of server processing
|
||
time and the client's round-trip correction is not systematically wrong. How
|
||
closely a given implementation can bracket that interval is not externally
|
||
observable; see Section 23.7.
|
||
|
||
**Protocol requirement.** The receive instant MUST NOT be later than the send
|
||
instant for the same reply. Measured server-side processing durations on
|
||
loopback were on the order of a few microseconds.
|
||
|
||
### 11.2 Clock Encodings
|
||
|
||
| Clock | Field | Encoding |
|
||
|-------|-------|----------|
|
||
| Wall | wall fields | Signed 64-bit little-endian, **nanoseconds since the Unix epoch** |
|
||
| Monotonic | monotonic fields | Signed 64-bit little-endian, **nanoseconds since an arbitrary server-chosen origin** |
|
||
|
||
**Observed monotonic origin.** The origin is fixed for the life of the server
|
||
process and is shared by every session and every listener of that process. Two
|
||
sessions opened seconds apart on different address families reported monotonic
|
||
values differing by exactly the elapsed wall time between them, and values
|
||
tracked the server process's uptime. A client therefore MUST NOT assume the
|
||
origin is per-session, and MUST NOT compare monotonic values against its own
|
||
clock — only differences between two server monotonic values from the same
|
||
server are meaningful.
|
||
|
||
**Protocol requirement.** A compatible server MAY choose any monotonic origin,
|
||
provided it is stable for as long as any session it reports into may live.
|
||
|
||
### 11.3 Absent Fields
|
||
|
||
A timestamp field that is not negotiated is simply not present in the reply; it
|
||
is not zero-filled. Field presence is positional — receivers derive offsets from
|
||
the negotiated parameters, not from the packet.
|
||
|
||
### 11.3.1 Upstream 0.9.1 — Dual-Field Midpoint Wire Representation
|
||
|
||
**Black-box observed.** For StampAt = Midpoint, upstream 0.9.1 emits the midpoint
|
||
as **two** 8-byte timestamp fields — the midpoint **wall** field followed by the
|
||
midpoint **monotonic** field, in that order — **even when the negotiated Clock
|
||
selects only one clock**. The midpoint occupies that fixed two-field, 16-byte
|
||
region whenever it is present at all, so a single-clock midpoint reply carries
|
||
one 8-byte field more than the negotiated parameters imply.
|
||
|
||
This is a statement about bytes on the wire. It says nothing about how upstream
|
||
represents a midpoint internally, and nothing in this section depends on any
|
||
such model.
|
||
|
||
**Which negotiations are affected.**
|
||
|
||
| StampAt | Clock | Midpoint region on the wire |
|
||
|---------|-------|-----------------------------|
|
||
| Midpoint (4) | Wall (1) | wall + monotonic — **one field more than negotiated** |
|
||
| Midpoint (4) | Monotonic (2) | wall + monotonic — **one field more than negotiated** |
|
||
| Midpoint (4) | Both (3) | wall + monotonic — matches the negotiated layout |
|
||
|
||
Receive-only, send-only and both-timestamp modes are **not** affected: each of
|
||
those emits exactly the fields the negotiated Clock implies, for all three Clock
|
||
values. Measured for all 15 StampAt × Clock combinations; only the two
|
||
single-clock midpoint rows deviate.
|
||
|
||
A client can reach this configuration without asking for midpoint timestamps: a
|
||
server restricted to a single timestamp answers a request for both receive and
|
||
send with Midpoint (Section 11.4), so a Wall-only client that requested StampAt =
|
||
Both also receives the dual-field form.
|
||
|
||
**Effect on reply length.** The extra field enlarges the *minimum* reply length;
|
||
it does **not** add 8 bytes to every reply. Reply length follows the ordinary
|
||
rule of Section 9.2 — the larger of the negotiated length and the field block —
|
||
applied to a field block one timestamp field larger. Writing `normal_header` for
|
||
the block of fields the negotiated parameters imply:
|
||
|
||
```
|
||
normal_header = negotiated field block
|
||
upstream_header = normal_header + one 8-byte timestamp field
|
||
|
||
compatible_reply_len = max(negotiated_length, normal_header)
|
||
upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)
|
||
```
|
||
|
||
The observable excess is therefore `upstream_0_9_1_reply_len −
|
||
compatible_reply_len`, which takes one of three forms depending on the
|
||
negotiated length:
|
||
|
||
| Negotiated length | Upstream 0.9.1 reply | Excess over a compatible server |
|
||
|-------------------|----------------------|---------------------------------|
|
||
| ≤ `normal_header` | `upstream_header` | **+8** |
|
||
| between `normal_header` and `upstream_header` | `upstream_header` | **+7 … +1**, shrinking as the negotiated length rises |
|
||
| ≥ `upstream_header` | exactly the negotiated length | **0** — the extra field displaces 8 bytes of payload |
|
||
|
||
Read the excess column across rows, not within one session: for any *single* set
|
||
of negotiated Params the upstream reply has exactly one length,
|
||
`upstream_0_9_1_reply_len`. The +8, +7…+1 and 0 values are what that one length
|
||
works out to under different negotiations. No negotiation admits a range of
|
||
lengths.
|
||
|
||
Measured with ReceivedStats = Both, StampAt = Midpoint, Clock = Wall
|
||
(`normal_header` 36, `upstream_header` 44):
|
||
|
||
| Negotiated length | 0 | 16 | 32 | 36 | 37 | 40 | 43 | 44 | 45 | 64 | 1024 | 4096 |
|
||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||
| Reply datagram | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 45 | 64 | 1024 | 4096 |
|
||
| Excess | +8 | +8 | +8 | +8 | +7 | +4 | +1 | 0 | 0 | 0 | 0 | 0 |
|
||
|
||
The same pattern was measured for ReceivedStats = None, Count and Window, with
|
||
the two minima shifting accordingly (24/32, 28/36 and 32/40 respectively). At a
|
||
negotiated length of 4096 the midpoint region still contains both fields,
|
||
followed by fill — so the dual-field emission is unconditional, and only its
|
||
effect on the *total datagram length* depends on the negotiated length.
|
||
|
||
**Correction to earlier material.** Any statement that an upstream reply is "the
|
||
negotiated length plus 8 bytes", or "the normal packet length plus 8", is wrong
|
||
as a general claim. It holds only in the first regime above. For sufficiently
|
||
large negotiated packets the datagram length is exactly the negotiated length,
|
||
and the excess field is visible only as displaced payload.
|
||
|
||
**Effect on the values a positional decoder reads.** Field offsets are derived
|
||
from the negotiated parameters, and the extra field is the last field before the
|
||
payload. The two forms compare as follows:
|
||
|
||
```
|
||
Clock = Wall
|
||
conforming single-clock form: [midpoint_wall][payload...]
|
||
upstream 0.9.1 form: [midpoint_wall][midpoint_mono][payload...]
|
||
|
||
Clock = Monotonic
|
||
conforming single-clock form: [midpoint_mono][payload...]
|
||
upstream 0.9.1 form: [midpoint_wall][midpoint_mono][payload...]
|
||
```
|
||
|
||
- **Clock = Wall:** the negotiated field comes first in both forms, so an
|
||
ordinary positional decoder reads the correct value.
|
||
- **Clock = Monotonic:** an ordinary positional decoder reads the first 8-byte
|
||
region — which upstream fills with the **wall-domain** value — as the
|
||
negotiated monotonic timestamp. Black-box verified with the upstream client: a
|
||
0.9.1 client requesting monotonic-only midpoint reported server monotonic
|
||
timestamps of approximately 1.79 × 10¹⁸ ns — a nanoseconds-since-epoch
|
||
magnitude, not a process-relative one — and completed the test without error.
|
||
|
||
**The two forms are not generally distinguishable.** Once the negotiated length
|
||
reaches `upstream_header`, the equality
|
||
|
||
```
|
||
upstream_0_9_1_reply_len == compatible_reply_len
|
||
```
|
||
|
||
holds, and the conforming and upstream forms produce datagrams of **identical
|
||
length**:
|
||
|
||
```
|
||
conforming: [mono][payload...]
|
||
upstream: [wall][mono][payload...]
|
||
```
|
||
|
||
In that regime nothing in the datagram identifies which form was used. The eight
|
||
bytes following the first midpoint region are a monotonic timestamp under the
|
||
upstream form and ordinary payload under a conforming one. This ambiguity is
|
||
recorded here as a limitation, not resolved: no heuristic for telling the two
|
||
apart in the equal-length regime has been established, and this document does not
|
||
invent one.
|
||
|
||
**Version behavior.** Black-box measured: upstream 0.9.0 emits only the
|
||
negotiated midpoint field for a single clock (a monotonic-only midpoint reply
|
||
carries a process-relative value at the negotiated length). The dual-field form
|
||
appears in 0.9.1 and is **unchanged** in the tested post-0.9.1 development tree
|
||
(Section 21).
|
||
|
||
**Source-assisted historical conclusion (non-normative).** Contaminated-side
|
||
investigation of upstream's release history indicates that the difference between
|
||
0.9.0 and 0.9.1 recorded above is an **unintended regression** rather than a
|
||
designed protocol feature. This is a judgement reached with access to upstream
|
||
material on the contaminated side; it is **not** an independent observation, and
|
||
the measured 0.9.0-versus-0.9.1 difference stated above does not by itself
|
||
establish intent. Nothing in upstream's published release notes or documentation
|
||
refers to the behavior. No requirement below depends on this paragraph, and a
|
||
reader who discards it entirely loses nothing normative.
|
||
|
||
**Protocol requirement.**
|
||
|
||
- A compatible server SHOULD emit exactly the fields implied by the negotiated
|
||
StampAt and Clock, including for midpoint. Emitting the conforming form is known
|
||
to interoperate: a contaminated-side server built to emit only the negotiated
|
||
midpoint field was driven by the upstream 0.9.1 client for Wall-only,
|
||
Monotonic-only and dual-clock midpoint configurations, and the client — an
|
||
independent implementation — completed every run cleanly.
|
||
- A compatible **client** MUST tolerate the verified upstream single-clock
|
||
midpoint dual-field form. Specifically, when the negotiated parameters are
|
||
StampAt = Midpoint with a single Clock, it MUST accept a reply whose length is
|
||
**exactly** `upstream_0_9_1_reply_len`. It MUST NOT require the extra field to
|
||
be present, and it MUST NOT treat its presence as an error.
|
||
- That tolerance is a **single additional accepted length, not a range**. For any
|
||
one set of negotiated Params, `compatible_reply_len` and
|
||
`upstream_0_9_1_reply_len` are two fixed numbers, and those are the only two
|
||
lengths an echo reply may have. When
|
||
`upstream_0_9_1_reply_len > compatible_reply_len` the upstream form is
|
||
identifiable by length alone, and the difference between the two values is
|
||
between 1 and 8 bytes. The +8, +7…+1 and 0 figures in the excess table earlier
|
||
in this section are differences **across different negotiations**; they are not
|
||
a range of acceptable lengths within one negotiation. A client MUST NOT apply a
|
||
rule of the form "accept anything up to 8 bytes over", and MUST reject a length
|
||
strictly between `compatible_reply_len` and `upstream_0_9_1_reply_len` exactly
|
||
as it rejects any other unexpected length.
|
||
- The tolerance is otherwise **narrow and specific**. It is not a general rule
|
||
that replies longer than expected are valid, and it does not license accepting
|
||
arbitrary overlong echo replies. A compatible client MUST continue to reject
|
||
replies *shorter* than `compatible_reply_len` — the normal compatible reply
|
||
length, which is the larger of the negotiated Length and the mandatory field
|
||
block, not the negotiated Length on its own — and MUST continue to apply its
|
||
ordinary strict validation to every other malformed or unexpectedly sized
|
||
datagram.
|
||
- A client that negotiates monotonic-only midpoint MAY read its monotonic value
|
||
from the second 8-byte region of the midpoint area **only when the dual-field
|
||
form is otherwise identifiable** — that is, when
|
||
`upstream_0_9_1_reply_len > compatible_reply_len` for the negotiated Params and
|
||
the datagram length is exactly `upstream_0_9_1_reply_len`, which happens only
|
||
while the negotiated length is below `upstream_header`. A client MUST NOT apply
|
||
that correction unconditionally
|
||
whenever midpoint and monotonic are negotiated: in the equal-length regime it
|
||
would interpret ordinary payload bytes from a conforming server as a timestamp.
|
||
- In the equal-length regime a client SHOULD record the value as ambiguous rather
|
||
than guess. Whatever it decides, it MUST still accept the packet: a reply is
|
||
not to be rejected merely because upstream might have produced the ambiguous
|
||
form, and a client that applies no correction MUST NOT fail on the resulting
|
||
implausible value.
|
||
- All of the above constrains a *client*. None of it is a licence, or a
|
||
requirement, for a clean implementation's **server** to emit the extra field.
|
||
|
||
**Accepted echo reply lengths, in full.** For a given set of negotiated Params,
|
||
compute `compatible_reply_len` and `upstream_0_9_1_reply_len` once. A conforming
|
||
client then accepts exactly these lengths and no others:
|
||
|
||
| Received `packet_len` | Condition | Outcome |
|
||
|---|---|---|
|
||
| `== compatible_reply_len` | always | **accepted** — the normal exact form |
|
||
| `== upstream_0_9_1_reply_len` | single-clock Midpoint **and** `upstream_0_9_1_reply_len > compatible_reply_len` | **accepted** — the identifiable upstream midpoint compatibility form |
|
||
| any other value | — | **rejected** |
|
||
|
||
When `upstream_0_9_1_reply_len == compatible_reply_len` the two rows collapse:
|
||
the packet is accepted at the normal exact length, the wire form is ambiguous
|
||
between `[mono][payload…]` and `[wall][mono][payload…]`, and no generic monotonic
|
||
correction is applied. This is behavioral wording; it prescribes no particular
|
||
decoder structure.
|
||
|
||
### 11.4 Timestamp Allowance (Server Policy)
|
||
|
||
The reference server can restrict which timestamp modes it will provide. The
|
||
observed mapping from requested StampAt to negotiated StampAt:
|
||
|
||
| Requested | Allowance = dual | Allowance = single | Allowance = none |
|
||
|-----------|------------------|--------------------|------------------|
|
||
| None (0) | 0 | 0 | 0 |
|
||
| Send (1) | 1 | 1 | 0 |
|
||
| Receive (2) | 2 | 2 | 0 |
|
||
| Both (3) | 3 | **4 (midpoint)** | 0 |
|
||
| Midpoint (4) | 4 | 4 | 0 |
|
||
|
||
Note the substitution: under a single-timestamp allowance, a request for both
|
||
receive and send is answered with **midpoint**, not with one of the two. Clients
|
||
comparing negotiated against requested parameters will see this as a server
|
||
restriction.
|
||
|
||
---
|
||
|
||
## 12. Server Timing Relationships
|
||
|
||
A server's timestamps exist so a client can separate server processing time from
|
||
network time. The relationships a compatible server MUST make derivable:
|
||
|
||
```
|
||
server_processing = server_send − server_receive (both from the same clock)
|
||
adjusted_rtt = raw_client_rtt − server_processing
|
||
send_delay = server_best_receive_wall − client_send_wall
|
||
receive_delay = client_receive_wall − server_best_send_wall
|
||
```
|
||
|
||
where *best receive* is the receive timestamp when present, otherwise the send
|
||
timestamp, and symmetrically for *best send*. For midpoint timestamps both
|
||
resolve to the midpoint value, which makes `server_processing` identically zero
|
||
— measured: an upstream client using midpoint timestamps reports a server
|
||
processing time of 0 s in all four statistics.
|
||
|
||
**Protocol requirement.**
|
||
|
||
- When both receive and send timestamps are negotiated, they MUST be taken from
|
||
the same clock domain, and MUST bracket the server's own handling of that
|
||
request.
|
||
- One-way delays are only meaningful with externally synchronized wall clocks;
|
||
a server has no obligation beyond reporting its own wall clock honestly.
|
||
- A server MUST NOT rewrite or smooth timestamps between packets.
|
||
|
||
Client-side statistics (IPDV, loss classification, aggregate reporting) are out
|
||
of scope here; see the client specification, Section 12.
|
||
|
||
---
|
||
|
||
## 13. Server Fill and Payload
|
||
|
||
### 13.1 What the Payload Is
|
||
|
||
Everything after the last present field in a reply, up to the reply's length, is
|
||
payload. Its content carries no protocol meaning; it exists to reach the
|
||
negotiated packet length.
|
||
|
||
### 13.2 Fill Negotiation
|
||
|
||
The ServerFill parameter (tag 9) is a length-prefixed UTF-8 string, maximum 32
|
||
bytes. Observed descriptor forms:
|
||
|
||
| Descriptor | Meaning |
|
||
|------------|---------|
|
||
| absent / empty | Client expresses no preference; the server uses its own configured fill |
|
||
| `none` | Do not fill; see Section 13.4 |
|
||
| `rand` | Fill with random bytes |
|
||
| `pattern:HH…` | Fill with the repeating byte pattern given as hex |
|
||
|
||
**Server policy.** The reference server matches the requested descriptor against
|
||
an allow-list of glob patterns (default: `rand` only). A descriptor that does
|
||
not match is replaced with the server's **default fill descriptor**, which is
|
||
returned to the client in the open reply.
|
||
|
||
Measured on a default-configuration server:
|
||
|
||
| Requested | Negotiated value returned | Actual reply payload |
|
||
|-----------|---------------------------|----------------------|
|
||
| `rand` | `rand` | random bytes |
|
||
| `none` | `pattern:69727474` | repeating `69 72 74 74` |
|
||
| `pattern:aabb` | `pattern:69727474` | repeating `69 72 74 74` |
|
||
| `bogus` | `pattern:69727474` | repeating `69 72 74 74` |
|
||
| 32-byte string | `pattern:69727474` | repeating `69 72 74 74` |
|
||
| 33-byte string | — | **open dropped, no reply** |
|
||
|
||
**Note.** The descriptor returned on refusal is the server's *default* fill
|
||
descriptor and does not necessarily describe the bytes the server will actually
|
||
send — a server configured with a different fill still returns the default
|
||
descriptor while filling with its configured fill. Clients MUST NOT rely on the
|
||
returned descriptor to predict payload bytes.
|
||
|
||
With a permissive allow-list, requested descriptors are honored and returned
|
||
verbatim: `pattern:aabb` produced `aa bb aa bb …`, `pattern:00` produced zeros,
|
||
`pattern:ff00` produced `ff 00 ff 00 …`. An unparseable hex body
|
||
(`pattern:zz`) falls back to the server default and returns
|
||
`pattern:69727474`.
|
||
|
||
### 13.3 Default Fill Bytes and Pattern Phase
|
||
|
||
**Upstream 0.9.1 behavior.** The default fill is the repeating four-byte pattern
|
||
`69 72 74 74`. The pattern's **phase is not reset per packet or per session**;
|
||
it advances continuously as bytes are consumed. Observed across four consecutive
|
||
replies with a 7-byte payload on one session:
|
||
|
||
```
|
||
seq 0 payload: 72 74 74 69 72 74 74
|
||
seq 1 payload: 69 72 74 74 69 72 74
|
||
seq 2 payload: 74 69 72 74 74 69 72
|
||
seq 3 payload: 74 74 69 72 74 74 69
|
||
```
|
||
|
||
and across separate sessions on the same listener, the phase carried over.
|
||
|
||
**Protocol requirement.** A client MUST NOT assume a reply payload begins at the
|
||
start of the fill pattern, and MUST NOT use payload bytes for any protocol
|
||
purpose. A compatible server MAY reset the phase per packet.
|
||
|
||
### 13.4 No Fill
|
||
|
||
**Upstream 0.9.1 behavior.** When the effective fill is `none`, no bytes are
|
||
written into the payload region. The bytes that appear there are unspecified
|
||
residual buffer content. In practice:
|
||
|
||
- when the request was at least as long as the reply, the client's own request
|
||
payload is returned;
|
||
- when the request was shorter, the trailing bytes are residue from earlier
|
||
traffic on that listener — observed to include fragments of a previous
|
||
packet's parameter payload.
|
||
|
||
The reference server's own documentation describes `none` as insecure on public
|
||
servers, and its default allow-list excludes it.
|
||
|
||
**Protocol requirement.** A compatible server that supports a no-fill mode MUST
|
||
NOT emit residual bytes from other clients' traffic. Zero-filling or echoing
|
||
only the bytes actually supplied by the requester are both acceptable.
|
||
|
||
### 13.5 Interaction with Length and Authentication
|
||
|
||
- The payload region is what remains after all present fields. Enabling
|
||
authentication consumes 16 bytes of the packet, reducing the payload for the
|
||
same negotiated length.
|
||
- If the negotiated length is smaller than the field block, there is no payload.
|
||
- The fill is applied after all fields are written, so it never overwrites
|
||
protocol data.
|
||
|
||
---
|
||
|
||
## 14. Authentication (HMAC)
|
||
|
||
### 14.1 Mechanism
|
||
|
||
When a server is configured with a key, an authentication field of 16 bytes
|
||
immediately follows the flags byte in **every** packet in both directions, and
|
||
the HMAC flag (0x08) is set.
|
||
|
||
The MAC is HMAC-MD5 over the **entire datagram** with the 16-byte
|
||
authentication field zeroed, exactly as specified in the client specification,
|
||
Section 13.4. A server computes and verifies it identically.
|
||
|
||
**Protocol requirement.** A server that is authenticating MUST set the HMAC flag
|
||
**and** write a valid MAC in every reply it sends, including echo replies. This
|
||
was verified the hard way: a server that authenticated open replies correctly
|
||
but omitted the HMAC flag from echo replies caused every echo reply to be
|
||
discarded by the upstream client, which then terminated abnormally with no
|
||
packets received.
|
||
|
||
### 14.2 Observed Behavior on Authentication Failure
|
||
|
||
Every case below results in a **silent drop**: no reply of any kind, and no
|
||
change to any session's state.
|
||
|
||
| Condition (server has a key) | Result |
|
||
|------------------------------|--------|
|
||
| Request has no HMAC flag and no field | Dropped |
|
||
| Request has the HMAC flag, MAC computed with a different key | Dropped |
|
||
| Request has the HMAC flag, one bit flipped in the MAC | Dropped |
|
||
| Request has the HMAC flag, MAC field all zeros | Dropped |
|
||
| Request too short to contain the MAC field (< 20 bytes) | Dropped |
|
||
| MAC field truncated (field shortened, rest shifted) | Dropped |
|
||
| MAC computed correctly but the HMAC flag cleared | Dropped |
|
||
| Valid open, valid echo, valid close | Served normally |
|
||
|
||
| Condition (server has **no** key) | Result |
|
||
|-----------------------------------|--------|
|
||
| Request has the HMAC flag set | Dropped |
|
||
|
||
**Protocol requirement.** Authentication failure MUST NOT produce a reply. A
|
||
server MUST reject a packet whose HMAC flag disagrees with its own
|
||
configuration, in **both** directions of mismatch. MAC comparison SHOULD be
|
||
constant-time.
|
||
|
||
Applies to all three request kinds: open, echo, and close. An unauthenticated
|
||
close request cannot tear down an authenticated session.
|
||
|
||
**Indistinguishability.** An authentication failure is externally
|
||
indistinguishable from every other drop: a packet bearing a *valid* token with a
|
||
bad MAC produces exactly the same silence as one bearing an unknown token.
|
||
|
||
**Protocol requirement.** A compatible server MUST NOT allow the result of a
|
||
session lookup to leak through its reply behavior for a packet that failed
|
||
authentication. Otherwise an unauthenticated peer could probe for live tokens.
|
||
|
||
### 14.3 Session Effect
|
||
|
||
An authentication failure leaves the session completely untouched. Measured: a
|
||
sequence of eight malformed or wrongly-keyed packets against a live session was
|
||
followed by a correctly authenticated echo that was served normally, with the
|
||
received count advancing only for the accepted packets.
|
||
|
||
---
|
||
|
||
## 15. CLOSE Lifecycle
|
||
|
||
### 15.1 Client-Initiated Close
|
||
|
||
**Request.** Direction client → server.
|
||
|
||
**Flags:** Close (0x04) set, Reply clear, Open clear, HMAC set iff
|
||
authenticating.
|
||
|
||
**Layout:** magic (3) + flags (1) + [authentication (16)] + token (8). Total
|
||
12 bytes unauthenticated, 28 bytes authenticated. There is no sequence number
|
||
and no payload.
|
||
|
||
**Observed server behavior:**
|
||
|
||
| Event | Result |
|
||
|-------|--------|
|
||
| Valid close from the session's endpoint | Session removed. **No reply of any kind is sent.** |
|
||
| Trailing bytes after the token | Accepted; session removed |
|
||
| Repeated close with the same token | Dropped as an unknown token; no reply |
|
||
| Echo after close | Dropped as an unknown token; no reply |
|
||
| Close from a foreign source port | Dropped; session **not** removed |
|
||
| Close bearing an unknown or zero token | Dropped; no reply |
|
||
|
||
**Protocol requirement.** A server MUST accept the close request and release the
|
||
session. It MUST NOT be expected to reply, and a compatible server that sends
|
||
nothing is fully conformant.
|
||
|
||
- **Upstream 0.9.1 sends no reply at all** to a normal client close. Verified in
|
||
`captures/server-close-lifecycle.pcapng`, where the close request is the last
|
||
packet in the exchange.
|
||
- Once the session is gone, a repeated close or a post-close echo is handled the
|
||
same way as any unknown or stale token: silently discarded, no reply.
|
||
- A standalone Reply|Close datagram is *tolerated* by upstream clients but has no
|
||
demonstrated purpose in protocol version 1. As a one-off experiment, an
|
||
upstream client was driven against a server that answered close with
|
||
magic + flags(Reply|Close) + [auth] + token; the run completed normally with
|
||
correct results and nothing in the client's output changed, showing no sign the
|
||
reply had been used. That establishes only that such a datagram is harmless.
|
||
|
||
**No production standalone close-reply codec is specified here.** Because no
|
||
version-1 client has been shown to do anything with such a datagram, this
|
||
document does not define one as required — or even as recommended — behavior. An
|
||
implementation that wants to emit one is not prohibited from doing so, but it
|
||
gains nothing observable and should not treat the shape above as a normative
|
||
encoding.
|
||
|
||
**The exact point at which the session becomes unavailable** is the moment the
|
||
close request is processed. The client has no acknowledgement and therefore no
|
||
way to distinguish "close delivered" from "close lost"; a lost close leaves the
|
||
session to be reclaimed by idle expiry (Section 18).
|
||
|
||
### 15.2 Server-Initiated Close
|
||
|
||
**When it happens.** A server signals close by setting the Close flag (0x04) on
|
||
an otherwise ordinary **echo reply**. There is no standalone server-initiated
|
||
close packet in protocol version 1.
|
||
|
||
**Upstream 0.9.1 behavior.** The single observed trigger is the maximum test
|
||
duration hard limit:
|
||
|
||
- when a maximum test duration is configured, the server clamps the negotiated
|
||
duration to it during negotiation;
|
||
- it additionally enforces a hard deadline of **maximum duration + 2 seconds**,
|
||
measured from the **first echo request** of the session;
|
||
- the first echo request arriving after that deadline is answered with a normal,
|
||
complete echo reply — correct sequence number, token, statistics and
|
||
timestamps — with the Close flag additionally set;
|
||
- the session is removed at the same time, so every later request is dropped as
|
||
an unknown token.
|
||
|
||
Measured: maximum duration 1 s → close flag on the reply at 3.01 s after the
|
||
first echo; maximum duration 2 s → close flag at 4.06 s and again at 4.1 s in a
|
||
separate run. Captured in `captures/server-close.pcapng` (frame 36 carries
|
||
flags `0x06`).
|
||
|
||
A conforming client never reaches this deadline, because it stops sending at the
|
||
negotiated duration. The mechanism exists to bound sessions from clients that
|
||
ignore the negotiated duration.
|
||
|
||
**Observed client reaction.** An upstream 0.9.1 client that receives an echo
|
||
reply with the Close flag set:
|
||
|
||
1. **does not record** that reply's measurement;
|
||
2. terminates the run immediately, reporting a "server closed connection"
|
||
condition;
|
||
3. prints the partial results gathered up to that point and exits with a success
|
||
status;
|
||
4. sends its own close request for the session.
|
||
|
||
**Protocol requirement.**
|
||
|
||
- A server that terminates a session mid-test MUST do so by setting the Close
|
||
flag on an echo reply; there is no other in-band mechanism.
|
||
- The close-flagged reply MUST otherwise be a well-formed echo reply for the
|
||
request that triggered it — clients parse it before noticing the flag.
|
||
- A server MUST NOT expect an acknowledgement. The client's subsequent close
|
||
request will be dropped as an unknown token, which is normal and harmless.
|
||
- The close-flagged reply MUST be authenticated when the session is
|
||
authenticated.
|
||
- The session token MUST be released at the same time; it MUST NOT remain usable
|
||
after a close-flagged reply.
|
||
|
||
---
|
||
|
||
## 16. No-Test Mode
|
||
|
||
A client may open and immediately close a session without running a test, in
|
||
order to validate parameters against a server.
|
||
|
||
**Request:** an open request with **both** Open (0x01) and Close (0x04) set.
|
||
|
||
**Reply:** Open | Reply | Close (0x07, or 0x0F with authentication), a **zero
|
||
token**, and the fully restricted parameter payload.
|
||
|
||
**Observed properties:**
|
||
|
||
| Property | Result |
|
||
|----------|--------|
|
||
| Session state created | **None.** No token is issued and nothing is retained |
|
||
| Token in the reply | All zeros |
|
||
| Parameters in the reply | The same restricted values a real open would return |
|
||
| Subsequent echo with the zero token | Dropped, no reply |
|
||
| Subsequent close with the zero token | Dropped, no reply |
|
||
| With authentication | Reply is authenticated normally; flags `0x0F` |
|
||
| Cleanup required | None |
|
||
|
||
**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
|
||
means "acknowledged and finished", not "rejected"; the distinction is made by
|
||
the client from whether *it* set Close in the request. See the client
|
||
specification, Sections 6.2 and 8.3.
|
||
|
||
---
|
||
|
||
## 17. Malformed, Unexpected and Adversarial Packets
|
||
|
||
Compact reference table. "Session effect: none" means the referenced session (if
|
||
any) is left fully usable.
|
||
|
||
| Request condition | Session state | Auth state | Server response | Session effect |
|
||
|---|---|---|---|---|
|
||
| Datagram shorter than the fixed header | any | any | drop | none |
|
||
| Bad magic | any | any | drop | none |
|
||
| Any reserved flag bit set (flags > 0x0F) | any | any | drop | none |
|
||
| Reply flag set | any | any | drop | none |
|
||
| HMAC flag set, server has no key | any | n/a | drop | none |
|
||
| No HMAC flag, server has a key | any | fail | drop | none |
|
||
| Wrong / corrupted / zeroed / truncated MAC | any | fail | drop | none |
|
||
| Open, truncated parameter varint | n/a | ok | drop | none |
|
||
| Open, duration ≤ 0 or interval ≤ 0 | n/a | ok | drop | none |
|
||
| Open, out-of-range stats / stamp-at / clock enum | n/a | ok | drop | none |
|
||
| Open, server-fill string > 32 bytes or short buffer | n/a | ok | drop | none |
|
||
| Open, unknown parameter tags | n/a | ok | **open reply**, tags ignored | new session |
|
||
| Open, empty parameter payload | n/a | ok | **open reply** | new session |
|
||
| Open, protocol version ≠ 1 | n/a | ok | **open reply with version 1** | new session |
|
||
| Open with Close flag (no-test) | n/a | ok | **reply, zero token** | none created |
|
||
| Non-open, too short for the token | any | ok | drop | none |
|
||
| Non-open, unknown token | — | ok | drop | none |
|
||
| Non-open, zero token | — | ok | drop | none |
|
||
| Echo, correct token, foreign source port | live | ok | drop | none |
|
||
| Close, correct token, foreign source port | live | ok | drop | **none — stays open** |
|
||
| Echo, too short for the sequence number | live | ok | drop | none |
|
||
| Echo exceeding the configured maximum length | live | ok | drop | none |
|
||
| Echo arriving faster than the rate allowance | live | ok | drop | none |
|
||
| Echo with trailing bytes past 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 lower sequence number than the last | live | ok | **reply** | count +1, window reset to `0x1` |
|
||
| Echo after the maximum-duration deadline | live | ok | **reply with Close flag** | session removed |
|
||
| Echo after idle expiry deadline | expired | ok | **reply** (served once) | session removed |
|
||
| Any request on a closed / expired session | gone | ok | drop | none |
|
||
| Close with trailing bytes | live | ok | **accepted**; no reply, as for any close | session removed |
|
||
| Echo-shaped datagram with the Open flag set, otherwise admissible | — | ok | treated as an **open**; the token bytes are parsed as parameters | new session if those bytes parse |
|
||
| Open flag set together with the Reply flag, or with an undefined flag bit | any | any | drop | none |
|
||
|
||
The last row restates the observable precedence of Section 5.4 in its narrow
|
||
form: for an otherwise admissible datagram, Open interpretation wins over Close
|
||
interpretation and over an echo-shaped body, so an echo-shaped datagram with the
|
||
Open flag set produces an open reply rather than an echo reply. That precedence
|
||
does not override the drop rows above — a datagram carrying the Open flag is
|
||
still dropped when the Reply flag is set, when an undefined flag bit is set, on
|
||
an authentication failure, or when its Open parameter payload is malformed or out
|
||
of range. A compatible server SHOULD behave the same way, so that a client's
|
||
retransmitted open is never mistaken for an echo.
|
||
|
||
---
|
||
|
||
## 18. Session Lifetime and Resource Policy
|
||
|
||
### 18.1 Lifetime Events
|
||
|
||
| Event | Effect |
|
||
|-------|--------|
|
||
| Open (non-no-test) | Session created, token issued |
|
||
| Open with Close (no-test) | No session created |
|
||
| Client close from the bound endpoint | Session removed immediately, no reply |
|
||
| Maximum test duration exceeded | Close-flagged echo reply, session removed (Section 15.2) |
|
||
| Idle expiry | Session removed (Section 18.2) |
|
||
| Server shutdown | All sessions gone, no notification |
|
||
| Client disappears | Nothing until idle expiry |
|
||
|
||
### 18.2 Idle Expiry — Observed Semantics
|
||
|
||
**Server policy.** The reference server has a configurable idle timeout,
|
||
default **1 minute**, with `0` meaning "never expire".
|
||
|
||
**Observed behavior**, measured with a 2-second idle timeout:
|
||
|
||
| Gap since the previous accepted echo | First request after the gap | The request after that |
|
||
|---|---|---|
|
||
| 5.0 s | answered | answered |
|
||
| 6.5 s | answered | answered |
|
||
| 6.9 s | answered | answered |
|
||
| 7.1 s | answered | **dropped** |
|
||
| 7.5 s | answered | **dropped** |
|
||
| 9.0 s | answered | **dropped** |
|
||
|
||
Two things follow, and both are externally observable:
|
||
|
||
1. **The effective expiry deadline is the configured idle timeout plus a
|
||
5-second grace period.** Measured boundary between 6.9 s and 7.1 s for a
|
||
2-second timeout.
|
||
2. **The first request that arrives after the deadline is still answered**, and
|
||
the session is torn down as part of handling it. Only the *next* request is
|
||
dropped. A compatible implementation MAY drop the first post-deadline request
|
||
instead; clients cannot depend on either behavior, because from their side
|
||
the deadline coincides with an unresponsive server.
|
||
|
||
**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.
|
||
Measured with a 2-second idle timeout: a session opened and left idle for 9
|
||
seconds with no echo request was still served afterwards. See Section 7.6 for
|
||
why this matters (open retransmissions).
|
||
|
||
**Protocol requirement.** Expiry itself is entirely server policy, and a
|
||
compatible server MAY choose any deadline or none. The one interoperability
|
||
constraint is negative: a server MUST NOT signal expiry to the client — the
|
||
client observes it only as silence.
|
||
|
||
**Robustness recommendation (clean implementation).** Apply an expiry deadline to
|
||
sessions that have never carried an echo request as well, rather than leaving
|
||
them resident indefinitely as upstream does.
|
||
|
||
Requests dropped for rate limiting still refresh the idle clock, so a client
|
||
sending faster than the allowance keeps its session alive without receiving
|
||
replies.
|
||
|
||
### 18.3 Resource Limits
|
||
|
||
**Observed upstream 0.9.1 policy.** The table below records what the reference
|
||
server was seen to do with its default configuration. Every row is an
|
||
observation about upstream's *policy*, not a protocol rule, and none of it is
|
||
behavior a compatible server is expected to reproduce.
|
||
|
||
| Limit | Observed |
|
||
|-------|----------|
|
||
| Maximum concurrent sessions | **None observed.** 1000 sessions from one source endpoint all succeeded and all remained usable |
|
||
| Per-peer session limit | **None** |
|
||
| Per-peer or global request rate limit | **None** beyond the per-session minimum-interval allowance |
|
||
| Maximum request length | Only if configured; unlimited by default |
|
||
| Maximum test duration | Only if configured; unlimited by default |
|
||
| Server-fill descriptor length | 32 bytes (hard) |
|
||
|
||
**Protocol requirement.** None. Nothing here is observable to a conforming
|
||
client, and a compatible server MAY impose session caps, per-peer caps and
|
||
admission limits freely. Because the protocol has no rejection reply, the only
|
||
way to refuse is to drop — a client hitting a cap sees an open timeout, which it
|
||
already has to tolerate.
|
||
|
||
**Explicitly not an interoperability requirement.** Upstream's absence of session
|
||
and per-peer bounds is an observed policy choice. It is **not** something a
|
||
compatible irtt-rs server should reproduce, and reproducing it would not improve
|
||
interoperability by any measure.
|
||
|
||
**Robustness recommendation (clean implementation).** A session is created by a
|
||
single unauthenticated datagram, opens are never deduplicated, and upstream never
|
||
expires a session that has not carried an echo request. The clean irtt-rs server
|
||
is therefore expected to operate under a **bounded** resource policy: bounded
|
||
total session state, and an expiry deadline that also covers never-used sessions.
|
||
The specific limits, the eviction strategy and the shape of that policy are a
|
||
clean-project design decision and are deliberately not prescribed here.
|
||
|
||
---
|
||
|
||
## 19. Protocol-Visible Limits and Defaults
|
||
|
||
Values marked *(protocol)* are fixed by the wire format. Values marked *(policy)*
|
||
are the reference server's defaults and are freely changeable by a compatible
|
||
implementation.
|
||
|
||
| Item | Value | Kind |
|
||
|------|-------|------|
|
||
| Magic | `0x14 0xA7 0x5B` | protocol |
|
||
| Flag bits defined | 0x01 Open, 0x02 Reply, 0x04 Close, 0x08 HMAC | protocol |
|
||
| Reserved flag bits | 0x10–0x80, must be zero | protocol |
|
||
| Token width | 64 bits, little-endian | protocol |
|
||
| Sequence width | 32 bits, little-endian, wraps modularly | protocol |
|
||
| Received count width | 32 bits, little-endian | protocol |
|
||
| Received window width | 64 bits, little-endian, bit 0 = current | protocol |
|
||
| Timestamp width | 64 bits signed, little-endian, nanoseconds | protocol |
|
||
| Authentication field | 16 bytes, HMAC-MD5, packet-wide with the field zeroed | protocol |
|
||
| Server-fill descriptor maximum | 32 bytes | protocol |
|
||
| Protocol version | 1 | protocol |
|
||
| Default port | 2112 | policy |
|
||
| Default minimum send interval | 10 ms | policy |
|
||
| Default burst allowance | 5 requests | policy |
|
||
| Default idle timeout | 1 minute | policy |
|
||
| Idle-expiry grace beyond the timeout | 5 seconds (measured) | policy |
|
||
| Maximum negotiable interval | idle timeout ÷ 4 (measured) | policy |
|
||
| Maximum-duration grace | 2 seconds (measured) | policy |
|
||
| Default maximum test duration | unlimited | policy |
|
||
| Default maximum packet length | unlimited | policy |
|
||
| Default fill pattern | repeating `69 72 74 74` | policy |
|
||
| Default fill allow-list | random fills only | policy |
|
||
| Default timestamp allowance | dual | policy |
|
||
| DSCP allowed by default | yes | policy |
|
||
|
||
**Client-side floor worth knowing.** Upstream clients reject any server-reduced
|
||
interval below **1 second**, in strict and loose mode alike. A server policy that
|
||
would produce a smaller interval makes the server unusable by those clients.
|
||
|
||
---
|
||
|
||
## 20. DSCP
|
||
|
||
**Protocol requirement — what the wire parameter is.** The DSCP parameter
|
||
(tag 8) carries the **raw IP TOS / Traffic Class byte**, with a useful range of
|
||
**0–255**. It is not a 6-bit codepoint. A server applies the negotiated value
|
||
directly as the TOS / Traffic Class byte of its echo replies, and a compatible
|
||
implementation MUST encode and interpret it the same way. The common Expedited
|
||
Forwarding marking is therefore **0xb8 (184)** on the wire, not 46. Verified by
|
||
capture; see `BLACKBOX_VERIFICATION_REPORT.md` Finding B and
|
||
`captures/dscp-session.pcapng`.
|
||
|
||
> **Out of scope: user-facing DSCP notation.** Whether an implementation's
|
||
> command line or configuration accepts a 6-bit codepoint (0–63) and shifts it
|
||
> left by two to produce the wire byte, or accepts the raw byte directly, is a
|
||
> presentation decision for that project. The clean irtt-rs project will decide
|
||
> and implement its own convention separately. Nothing about that choice is
|
||
> upstream wire behavior, and it does not belong in this specification.
|
||
|
||
**Server policy.** When DSCP is permitted and supported on the socket, the server
|
||
applies the negotiated value to its echo replies. When DSCP is disallowed, the
|
||
negotiated value is forced to 0 and returned as absent from the parameter
|
||
payload.
|
||
|
||
**Observed range handling.** The negotiated value is **not** range-checked
|
||
during negotiation. Values 0–255 are applied normally. A value above 255 is
|
||
accepted and echoed back in the open reply, but then every echo reply for that
|
||
session fails to send and is dropped — the client sees a session that opens and
|
||
then answers nothing.
|
||
|
||
**Robustness recommendation (clean implementation).** Reject or clamp DSCP
|
||
values outside 0–255 during negotiation rather than accepting a value that cannot
|
||
be applied, so that the value returned in the open reply is one the server will
|
||
honour.
|
||
|
||
DSCP is applied to echo replies only; open and close packets are unmarked (see
|
||
the client specification, Section 10.8).
|
||
|
||
---
|
||
|
||
## 21. Version Differences
|
||
|
||
### 21.1 0.9.0 → 0.9.1
|
||
|
||
Two independent things are recorded here and must not be conflated.
|
||
|
||
**What upstream's published release notes say.** They cover platform time
|
||
precision, server logging transport, a probe integration, build changes and bug
|
||
fixes. None of it is presented as a protocol change, and the protocol version
|
||
remained 1. Taken alone, the release notes would suggest no wire-visible
|
||
difference.
|
||
|
||
**What was measured on the wire.** One wire-visible difference exists that the
|
||
release notes do not mention: the single-clock midpoint layout. A 0.9.0 server
|
||
emits only the negotiated midpoint field; a 0.9.1 server emits both midpoint
|
||
fields. Measured side by side with identical scripted sessions:
|
||
|
||
| Server | StampAt = 4, Clock = Wall | StampAt = 4, Clock = Monotonic | StampAt = 4, Clock = Both |
|
||
|--------|---------------------------|--------------------------------|---------------------------|
|
||
| 0.9.0 | 36 bytes, one field | 36 bytes, one field, process-relative value | 44 bytes, two fields |
|
||
| 0.9.1 | **44 bytes, two fields** | **44 bytes, two fields** | 44 bytes, two fields |
|
||
|
||
(Received stats = both, negotiated length 0.) See Section 11.3.1.
|
||
|
||
**Correction to earlier material.** Any earlier statement that there were no
|
||
protocol-visible changes between 0.9.0 and 0.9.1 is wrong. That claim followed
|
||
the release notes rather than measurement; the measured difference above stands.
|
||
|
||
**Classification: one wire-visible difference in 0.9.1, alongside changes that
|
||
are not wire-visible.** Section 11.3.1 additionally records a source-assisted,
|
||
non-normative judgement that the wire-visible difference is unintended. That
|
||
judgement is separate from, and not required by, the measurement above.
|
||
|
||
### 21.2 0.9.1 → the tested post-0.9.1 development tree
|
||
|
||
**Provenance.** The development tree exercised for this study is the 0.9.1
|
||
release plus **six** subsequent upstream commits on the development branch: an
|
||
ECN change and its merge, a roadmap note, a documentation correction, and a
|
||
dependency update. Only one of them is a functional change. These six commits are
|
||
**not** part of the 0.9.1 release; everything in this subsection was measured
|
||
against a build of that development tree, and nothing here describes 0.9.1.
|
||
|
||
**Change: an opt-in server ECN option.**
|
||
|
||
- Adds a server command-line option that, per its documentation, ships ECN bits
|
||
for the client to log, and states that it is IPv6-only, that it forces the
|
||
source-IP-pinning option, and that it "disables UDP replies from server".
|
||
- **Observed behavior contradicts the documentation on the last point.** With
|
||
the option enabled, sessions completed normally over both IPv4 and IPv6:
|
||
packets sent/received 4/4, no loss, ordinary results. Replies are **not**
|
||
disabled.
|
||
- Enabling it forces the source-IP-pinning behavior described in Section 5.3.
|
||
- ECN observation is a receive-side property read from the IP header; it does
|
||
not change the IRTT payload in either direction.
|
||
|
||
**Timestamp layout is unchanged.** The full StampAt × Clock matrix and the
|
||
midpoint length sweep of Section 11.3.1 were re-measured against a build of this
|
||
development tree and produced results identical to 0.9.1, byte-position for
|
||
byte-position. The tested post-0.9.1 tree therefore behaves like 0.9.1 in this
|
||
respect, and none of the six commits changes the dual-field midpoint emission.
|
||
|
||
**Wire comparison.** A side-by-side run of the 0.9.1 release build and the
|
||
post-0.9.1 build, using an identical scripted request sequence (open with full
|
||
statistics and dual timestamps on both clocks; echo sequence 0, 1, 4, 3; close),
|
||
produced byte-identical structure: same open-reply layout, same reply lengths
|
||
(60 bytes), same flags, same counts (1, 2, 3, 4) and the same windows
|
||
(`0x1`, `0x3`, `0x19`, `0x1`), and no close reply in either case.
|
||
|
||
**Classification: CLI / server-policy difference. Not wire-visible.**
|
||
|
||
Nothing in this document changes for the tested post-0.9.1 tree.
|
||
|
||
### 21.3 0.1.x
|
||
|
||
Incompatible predecessor protocol. Out of scope.
|
||
|
||
---
|
||
|
||
## 22. Implementation Hazards Observed Upstream
|
||
|
||
These are behaviors a clean implementation should deliberately **not**
|
||
reproduce. They are listed because each is reachable from ordinary client
|
||
traffic, so a clean server has to decide what to do instead.
|
||
|
||
Everything in this section is an **observation plus a robustness
|
||
recommendation**. None of it is an interoperability requirement: no conforming
|
||
client can tell which choice a server made, because in every case the visible
|
||
effect is either silence or an ordinary reply.
|
||
|
||
### 22.1 Oversized Reply Terminates the Server
|
||
|
||
If the negotiated packet length exceeds the host's maximum outbound UDP
|
||
datagram size, the first echo reply fails to send with a non-transient error,
|
||
and upstream 0.9.1 treats that as fatal: the listener stops and the server
|
||
process exits. Observed on a host whose maximum outbound datagram size was
|
||
9216 bytes: a session negotiating 15000 bytes killed the server on its first
|
||
echo reply, and all subsequent clients received nothing.
|
||
|
||
The default configuration places no upper bound on the negotiated length, so
|
||
this is reachable without any malformed packet.
|
||
|
||
**Robustness recommendation (restated from Section 6.3).** Treat a per-packet
|
||
send failure as a per-packet event rather than a fatal one, and refuse to
|
||
negotiate a length the server cannot transmit.
|
||
|
||
### 22.2 Zero-Length Fill Pattern Terminates the Server
|
||
|
||
A server-fill descriptor of the form `pattern:` with an empty hex body
|
||
terminates upstream 0.9.1 during open handling, when the fill allow-list permits
|
||
pattern fills. The default allow-list does not, so a default-configured server
|
||
is not exposed; a server configured with a permissive allow-list is.
|
||
|
||
**Robustness recommendation.** Validate a fill descriptor before using it and
|
||
reject a zero-length pattern. Falling back to the server default — the behavior
|
||
already observed for an unparseable hex body such as `pattern:zz` — is the
|
||
natural response, and is what upstream already does for that neighbouring case.
|
||
|
||
### 22.3 Negotiated Length Not Bounded by Path or Interface
|
||
|
||
The negotiated length reported to the client is not clamped to the interface
|
||
MTU, but the reply is (Section 9.2). Clients treat a short reply as fatal, so
|
||
this produces a session that opens successfully and then fails immediately.
|
||
|
||
**Robustness recommendation.** Clamp the negotiated length to what the server can
|
||
actually emit, so that the value returned in the open reply is honest. (The
|
||
resulting reply length is then covered by the ordinary rule of Section 9.2, which
|
||
*is* an interoperability requirement — a reply shorter than the negotiated length
|
||
is fatal to upstream clients.)
|
||
|
||
### 22.4 Residual Payload Disclosure Under No-Fill
|
||
|
||
See Section 13.4. A no-fill mode that emits uninitialized buffer content can
|
||
leak bytes from other clients' traffic. The requirement not to emit another
|
||
peer's data is stated in Section 13.4; it is a security property of the
|
||
implementation rather than a wire-compatibility rule.
|
||
|
||
---
|
||
|
||
## 23. Remaining Unknowns
|
||
|
||
Each entry records the question, why compatibility might depend on it, what was
|
||
inspected, what was attempted, and what evidence would settle it.
|
||
|
||
### 23.1 Received-count wraparound
|
||
|
||
- **Question:** What does the 32-bit received count do at 2^32 requests in a
|
||
single session — wrap, saturate, or something else?
|
||
- **Why it matters:** A client computing upstream loss from the count would see
|
||
a large negative or nonsensical delta at the wrap point. Only reachable in
|
||
extremely long, high-rate sessions.
|
||
- **Evidence inspected:** Field width and encoding confirmed from captures;
|
||
counter observed to advance by exactly one per accepted request.
|
||
- **Attempted:** Not attempted. Reaching 2^32 requests is not feasible in a
|
||
targeted experiment.
|
||
- **Would resolve it:** A server-side test harness able to preload a session
|
||
counter, or a very long-running instrumented session.
|
||
|
||
### 23.2 Behavior under genuine resource exhaustion
|
||
|
||
- **Question:** What does a server do when session state or buffers can no
|
||
longer be allocated?
|
||
- **Why it matters:** Determines whether a client sees an open timeout, a
|
||
partial session, or a dead server.
|
||
- **Evidence inspected:** 1000 concurrent sessions from one endpoint were
|
||
created with no refusals and no degradation; no admission control was
|
||
observed.
|
||
- **Attempted:** Scaling to exhaustion was deliberately not attempted — it is a
|
||
resource-starvation test, not a protocol test.
|
||
- **Would resolve it:** A memory-capped container running the reference server
|
||
while a controlled harness opens sessions until allocation fails.
|
||
|
||
### 23.3 IPv6 zone / link-local session identity
|
||
|
||
- **Question:** Is the IPv6 zone index part of session identity in practice, and
|
||
how are scoped addresses compared?
|
||
- **Why it matters:** A server bound to a link-local address on multiple
|
||
interfaces could confuse two peers with the same address in different zones.
|
||
- **Evidence inspected:** IPv6 sessions over loopback work identically to IPv4;
|
||
the IPv4 and IPv6 listeners of one port were confirmed to have separate token
|
||
scopes.
|
||
- **Attempted:** Link-local multi-interface testing was not possible on the
|
||
single-host test platform.
|
||
- **Would resolve it:** Two interfaces with the same link-local address in
|
||
different zones, with clients on each.
|
||
|
||
### 23.4 Midpoint dual-field emission and non-upstream clients
|
||
|
||
- **Question:** Do client implementations other than upstream 0.9.1 tolerate the
|
||
extra midpoint field, and do any require it?
|
||
- **Why it matters:** Section 11.3.1 recommends emitting only the negotiated
|
||
fields; a client that computes an exact expected reply length and rejects
|
||
longer replies would break against upstream servers, not against a compatible
|
||
one.
|
||
- **Evidence inspected:** All 60 combinations of ReceivedStats × StampAt × Clock
|
||
measured against the reference server; a negotiated-length sweep across the two
|
||
minima for four received-stats settings; upstream client verified to accept
|
||
both forms. The 0.9.0 build and the tested post-0.9.1 build were measured for
|
||
comparison.
|
||
- **Note:** the question is narrower than it was. Because the length difference
|
||
disappears once the negotiated length reaches `upstream_header`, a third-party
|
||
client is only exposed to the length discrepancy at small negotiated lengths;
|
||
at larger ones it is exposed only to the field *contents* in the monotonic-only
|
||
case — which is also the regime in which the two forms cannot be told apart
|
||
(Section 11.3.1).
|
||
- **Attempted:** Only upstream 0.9.1 was available as a second implementation.
|
||
- **Would resolve it:** Testing against a third-party client, or a survey of
|
||
other implementations.
|
||
|
||
### 23.5 Reply-length capping on other platforms
|
||
|
||
- **Question:** Is the reply-length cap always the listener interface's MTU, or
|
||
does it vary with how the platform reports interface MTUs?
|
||
- **Why it matters:** It determines the largest length a server can honestly
|
||
negotiate.
|
||
- **Evidence inspected:** Measured on one host: replies capped at exactly the
|
||
bind interface's MTU (1500 on an Ethernet interface); loopback with a
|
||
16384-byte MTU was masked by a smaller maximum outbound datagram size.
|
||
- **Attempted:** Two interfaces with different MTUs on one host.
|
||
- **Would resolve it:** The same measurement on Linux and Windows hosts with
|
||
jumbo-frame and tunnel interfaces.
|
||
|
||
### 23.6 Whether any trigger other than the duration limit sets Close on a reply
|
||
|
||
- **Question:** Are there conditions besides the maximum-duration hard limit
|
||
that cause upstream to set the Close flag on an echo reply?
|
||
- **Why it matters:** A client must handle the flag whenever it appears; a
|
||
server implementer needs to know which conditions warrant it.
|
||
- **Evidence inspected:** The duration limit was reproduced and captured. Idle
|
||
expiry, shutdown, rate limiting, oversize requests and authentication failure
|
||
were each exercised and none of them produced a close-flagged reply.
|
||
- **Attempted:** All the above, plus repeated close and foreign-endpoint close.
|
||
- **Would resolve it:** Exhaustive exercise of every server policy knob against
|
||
a rogue client that keeps sending regardless.
|
||
|
||
### 23.7 How tightly server timestamps bracket the request
|
||
|
||
- **Question:** How much of a server's own send and receive path falls outside
|
||
the interval its two timestamps describe?
|
||
- **Why it matters:** It bounds the accuracy of the client's server-processing
|
||
correction.
|
||
- **Evidence inspected:** Receive-to-send deltas of a few microseconds on
|
||
loopback; ordering (receive ≤ send) always held.
|
||
- **Attempted:** Cannot be separated from scheduling noise externally.
|
||
- **Would resolve it:** Hardware timestamping on both endpoints. Nothing about
|
||
where an implementation takes its timestamps is externally observable, so this
|
||
is likely to remain permanently open, and no requirement in this document
|
||
depends on it. Section 11.1 states the relationship a compatible server should
|
||
aim to make derivable.
|
||
|
||
---
|
||
|
||
## 24. Conformance Summary
|
||
|
||
The two lists below are **interoperability** requirements only: each is
|
||
observable by a conforming client, and each rests on a black-box experiment.
|
||
Robustness recommendations for the clean implementation are collected separately
|
||
in Section 24.3 and are deliberately not mixed in.
|
||
|
||
### 24.1 A conforming server MUST:
|
||
|
||
1. Speak UDP, validate magic and flag bits, and reject any packet with the Reply
|
||
flag set.
|
||
2. Set the Reply flag on every packet it emits, and never the Open flag on an
|
||
echo reply.
|
||
3. Silently drop every rejected packet — no error replies, ever.
|
||
4. Accept open requests without a token field, decode the parameter payload per
|
||
the shared encoding rules, and return the **restricted** parameters together
|
||
with a non-zero token.
|
||
5. Return ProtocolVersion = 1 in the open reply.
|
||
6. Return a zero token and the Close flag for a no-test open, and create no
|
||
session for it.
|
||
7. Bind each session to its token **and** its source endpoint, and drop packets
|
||
that fail either check without disturbing the session.
|
||
8. Echo the token and sequence number unchanged in every echo reply.
|
||
9. Emit the negotiated fields in the specified order, and pad to the negotiated
|
||
length. For StampAt = Midpoint this means the fields the negotiated Clock
|
||
implies and no others — a conforming server does not reproduce the upstream
|
||
dual-field emission of Section 11.3.1.
|
||
10. Report a received count that includes the current request and excludes every
|
||
dropped one.
|
||
11. Report a received window with bit 0 set, bit *k* meaning "sequence
|
||
(current − *k*) was received".
|
||
12. Emit receive and send timestamps from a consistent clock domain, such that
|
||
the pair brackets the server's own handling of that request and the receive
|
||
instant is never later than the send instant.
|
||
13. Release the session on a valid close request. Sending nothing in response is
|
||
correct and is what upstream does; no close-reply datagram is required, and
|
||
none is specified here (Section 15.1).
|
||
14. Signal a server-initiated close only by setting the Close flag on an
|
||
otherwise complete echo reply, and release the token at that point.
|
||
15. When authenticating: set the HMAC flag and a valid MAC on **every** reply,
|
||
and drop silently on any authentication failure, with no reply behavior that
|
||
could distinguish a known token from an unknown one.
|
||
|
||
### 24.2 A conforming server MUST NOT:
|
||
|
||
1. Reply to any malformed, unauthenticated, unknown-token or foreign-endpoint
|
||
packet.
|
||
2. Interpret any request bytes beyond the token and sequence number.
|
||
3. Emit another peer's data, or uninitialized memory, as payload.
|
||
4. Return a negotiated packet length in the open reply that it will not
|
||
actually emit.
|
||
|
||
### 24.3 Robustness recommendations for the clean implementation
|
||
|
||
Not interoperability requirements. No conforming client can observe whether these
|
||
were followed; they are here because each addresses a behavior observed upstream
|
||
that a new implementation should not copy.
|
||
|
||
1. Treat a per-packet send failure as a per-packet event, not a fatal one
|
||
(Sections 6.3, 22.1).
|
||
2. Operate under a bounded session-resource policy, and expire sessions that have
|
||
never carried an echo request (Sections 7.6, 18.2, 18.3). The specific limits
|
||
and eviction policy are a clean-project decision.
|
||
3. Validate a fill descriptor before use, and reject a zero-length pattern
|
||
(Section 22.2).
|
||
4. Reject or clamp DSCP values the socket cannot carry, during negotiation
|
||
(Section 20).
|
||
5. Clamp the negotiated length to what the server can actually transmit
|
||
(Sections 22.1, 22.3).
|
||
|
||
---
|
||
|
||
## 25. Related Documents
|
||
|
||
- `IRTT_CLIENT_PROTOCOL_SPEC.md` — wire format, parameter encoding, HMAC
|
||
computation, client-side measurement semantics.
|
||
- `BLACKBOX_VERIFICATION_REPORT.md` — verification evidence, including the
|
||
server-side section.
|
||
- `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` — input → output vectors for this
|
||
document.
|
||
- `test-vectors/README.md` — packet-level test vectors.
|
||
- `captures/` — packet captures referenced throughout.
|
||
- `../clean-room/CLEANROOM_NOTES.md` — clean-room boundary and audit record.
|