docs(protocol): add sanitized server compatibility research
This commit is contained in:
20 files changed
+4213
-175
No files matched your search
@@ -10,23 +10,22 @@
|
||||
|
||||
## Environment Check
|
||||
|
||||
```
|
||||
$ command -v irtt
|
||||
/Users/lochnair/.local/bin/irtt
|
||||
Tooling used for Part I. Installation paths are omitted deliberately; only the
|
||||
versions matter for reproducing these results.
|
||||
|
||||
```
|
||||
$ irtt version
|
||||
irtt version: 0.9.1
|
||||
protocol version: 1
|
||||
json format version: 1
|
||||
go version: go1.26.2 on darwin/arm64
|
||||
|
||||
$ command -v tshark
|
||||
/opt/homebrew/bin/tshark
|
||||
|
||||
$ tshark --version
|
||||
TShark (Wireshark) 4.6.4 (Git commit f7c4a74874d9).
|
||||
TShark (Wireshark) 4.6.4
|
||||
```
|
||||
|
||||
Reference server and client: the upstream **0.9.1 release**, unmodified.
|
||||
Capture platform: macOS Darwin 25.3.0 arm64, loopback interface.
|
||||
|
||||
---
|
||||
|
||||
## Open Question Resolution
|
||||
@@ -126,6 +125,11 @@ Timestamp field count depends on StampAt and Clock:
|
||||
This is a **normative requirement** — implementations must compute the
|
||||
correct minimum to avoid malformed packets.
|
||||
|
||||
**Caveat added 2026-08-11:** the two single-clock midpoint rows describe the
|
||||
*negotiated* layout, which is what a compatible implementation emits. Upstream
|
||||
0.9.1 emits 2 fields (16 bytes) for midpoint with any clock; see Findings S-7 and
|
||||
S-18.
|
||||
|
||||
---
|
||||
|
||||
### 19.4 Varint Encoding — RESOLVED
|
||||
@@ -154,7 +158,8 @@ verified against known values.
|
||||
|
||||
All decoded correctly using standard protobuf-style zigzag + LEB128.
|
||||
|
||||
**Verified independently with Python decoder.**
|
||||
**Cross-checked with a separately written Python decoder**, implemented from the
|
||||
wire-format description alone.
|
||||
|
||||
**Conclusion:** The varint encoding is standard protobuf-compatible. This
|
||||
is a **normative requirement** for interoperability.
|
||||
@@ -166,8 +171,8 @@ is a **normative requirement** for interoperability.
|
||||
**Question:** Is the HMAC computed over the entire packet (including payload)
|
||||
with the HMAC field zeroed?
|
||||
|
||||
**Test setup:** Captured HMAC session, extracted packet bytes, independently
|
||||
computed HMAC-MD5 with Python.
|
||||
**Test setup:** Captured HMAC session, extracted packet bytes, and recomputed
|
||||
HMAC-MD5 with a separately written Python script.
|
||||
|
||||
**Commands:**
|
||||
```
|
||||
@@ -228,6 +233,11 @@ or a bug.
|
||||
can be used as an invalidity sentinel. This is a **compatibility
|
||||
recommendation** — implementations SHOULD treat window=0 as invalid.
|
||||
|
||||
**Superseded in part by Finding S-2 (Part II).** Upstream 0.9.1 was later
|
||||
confirmed never to emit 0 at all when the window is negotiated. The value that
|
||||
actually needs care is `0x1`, which is a *valid* window carrying no history and
|
||||
must not be read as "nothing earlier was received".
|
||||
|
||||
---
|
||||
|
||||
### 19.8 HMAC Mismatch Behavior — RESOLVED
|
||||
@@ -308,6 +318,11 @@ ServerFill string (32 bytes max), the parameter buffer stays well under
|
||||
Reclassified as **compatibility recommendation** — implementations should
|
||||
allocate at least 128 bytes for the parameter buffer.
|
||||
|
||||
**Superseded by Part II.** Upstream servers were later shown to accept an open
|
||||
request carrying over 200 bytes of parameters. There is no protocol limit on a
|
||||
*received* payload; 128 bytes is a local allocation choice only, and a receiver
|
||||
should tolerate more rather than reject.
|
||||
|
||||
---
|
||||
|
||||
### 19.11 Field Ordering Verification — RESOLVED
|
||||
@@ -344,22 +359,26 @@ allocate at least 128 bytes for the parameter buffer.
|
||||
|
||||
---
|
||||
|
||||
### 19.3 Server Close During Test — NOT DIRECTLY TESTED
|
||||
### 19.3 Server Close During Test — NOT DIRECTLY TESTED (superseded)
|
||||
|
||||
**Limitation:** Triggering a server-initiated close mid-test requires
|
||||
specific server conditions that are difficult to set up reliably in a
|
||||
**Limitation at the time:** Triggering a server-initiated close mid-test requires
|
||||
specific server conditions that were difficult to set up reliably in a
|
||||
black-box test. The spec's description (server MAY set Close flag on echo
|
||||
reply) has not been verified externally. Reclassified as **open /
|
||||
verification required** — the spec now recommends defensive handling
|
||||
without asserting that this behavior occurs.
|
||||
reply) had not been verified externally, and was reclassified as **open /
|
||||
verification required**.
|
||||
|
||||
**Resolved in Part II by Finding S-4.** The maximum-test-duration hard limit was
|
||||
found to trigger it reliably, and the behavior was captured. This entry is
|
||||
retained only as a record of the earlier state.
|
||||
|
||||
---
|
||||
|
||||
### 19.6 Send Timestamp Capture Timing — NOT TESTABLE
|
||||
|
||||
**Limitation:** The exact timing of client-side timestamp capture relative
|
||||
to the send syscall is an implementation-internal detail that cannot be
|
||||
verified through black-box testing. Remains an implementation choice.
|
||||
**Limitation:** How closely a client's own timestamp capture sits to its send
|
||||
and receive operations is internal to that client and cannot be verified through
|
||||
black-box testing, in either direction. Remains an implementation choice, and
|
||||
nothing normative depends on it.
|
||||
|
||||
---
|
||||
|
||||
@@ -497,8 +516,8 @@ open request.
|
||||
### Finding E: Test Vector Size Correction
|
||||
|
||||
The test vectors in Section 18 are described as "target length 256 bytes
|
||||
(shown truncated)." Independent HMAC verification confirms that both test
|
||||
vectors (18.1 and 18.2) were computed over 92-byte packets (76-byte header
|
||||
(shown truncated)." Recomputing the HMAC from the captured bytes confirms that
|
||||
both vectors (18.1 and 18.2) were computed over 92-byte packets (76-byte header
|
||||
+ 16-byte payload), not 256-byte packets. The "256 bytes" description is
|
||||
incorrect.
|
||||
|
||||
@@ -560,6 +579,12 @@ irtt client -l 200 127.0.0.1 # request length 200
|
||||
|
||||
## Captures Created
|
||||
|
||||
All capture files referenced in this report were metadata-reviewed and their
|
||||
containers rewritten to strip capture comments, section and interface
|
||||
descriptions, host/user/machine identification, capture filter strings and
|
||||
filesystem paths. Packet bytes and timestamps were not altered — full frame
|
||||
hexdumps and per-frame epoch timestamps were verified identical before and after.
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `captures/full-session.pcapng` | Complete session: open, 3 echoes, close |
|
||||
@@ -608,3 +633,551 @@ See `test-vectors/` directory.
|
||||
defensive handling without asserting the behavior occurs
|
||||
6. Received window late-packet behavior: removed unverified claim about
|
||||
late packets producing window=0; retained verified bit-0 sentinel
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
# Part II — Server-Side Verification
|
||||
|
||||
**Date:** 2026-08-11
|
||||
**Baseline:** the upstream **0.9.1 release** (protocol version 1)
|
||||
**OS:** macOS Darwin 25.5.0 arm64 (Apple Silicon)
|
||||
|
||||
**Method, and what each part of it establishes:**
|
||||
|
||||
- **Black-box observation.** A raw UDP harness drove a real upstream server
|
||||
directly, so that requests no conforming client would emit could be tested. It
|
||||
shares no code with any implementation. Everything it measured is stated here
|
||||
as observed wire behavior.
|
||||
- **Black-box observation at the application layer.** The real upstream 0.9.1
|
||||
*client* was driven against various servers, and its externally visible
|
||||
reaction — what it recorded, what it reported, how it exited — was observed.
|
||||
The client is an independent second implementation for this purpose.
|
||||
- **Contaminated-side consistency validation.** A server was also written on the
|
||||
contaminated side from the behavioral model and driven by the real upstream
|
||||
client. Because it was produced on this side of the boundary it is **not** an
|
||||
independent implementation, and no statement in this report rests on it. What
|
||||
it provides is a consistency check: it demonstrates that the model as written
|
||||
is sufficient to drive a real upstream client. The *client's* reaction to it is
|
||||
black-box observation; the server itself is not.
|
||||
|
||||
Findings feed `IRTT_SERVER_PROTOCOL_SPEC.md`. Input → output tables are in
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md`.
|
||||
|
||||
---
|
||||
|
||||
## Part II Environment
|
||||
|
||||
```
|
||||
$ irtt version
|
||||
irtt version: 0.9.1
|
||||
protocol version: 1
|
||||
json format version: 1
|
||||
```
|
||||
|
||||
**Provenance of the second binary.** A second server binary was built from the
|
||||
upstream development tree as it stood **six upstream commits past the 0.9.1
|
||||
tag**, for the version-difference comparison in Finding S-13. Those six commits
|
||||
are post-release work and are **not** part of 0.9.1; every result attributed to
|
||||
that build is labelled as such and never folded into the 0.9.1 baseline.
|
||||
|
||||
*(An earlier revision of this report said "8 commits past the 0.9.1 tag". That
|
||||
count was wrong: it included two commits belonging to this clean-room project
|
||||
itself, which were present in the same working tree but are not upstream work.
|
||||
The upstream count is six.)*
|
||||
|
||||
---
|
||||
|
||||
## Finding S-1: Session identity is token + source endpoint
|
||||
|
||||
A post-open request is bound to a session by its token **and** by an exact match
|
||||
on the source address, source port, and address family.
|
||||
|
||||
| Case | Result |
|
||||
|------|--------|
|
||||
| Correct token, original endpoint | Served |
|
||||
| Correct token, different source port | **Dropped, no reply; session intact** |
|
||||
| Correct token, other address family's listener on the same port | Dropped |
|
||||
| Unknown token | Dropped |
|
||||
| Zero token | Dropped |
|
||||
| Token of a closed session | Dropped |
|
||||
| **Close** bearing a valid token from a foreign source port | **Dropped; session not closed** |
|
||||
|
||||
Endpoint binding protects the close path as well as the echo path. A capture is
|
||||
in `captures/server-session-identity.pcapng`.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-2: Received window resets to `0x1` on reordering and large gaps
|
||||
|
||||
The single most consequential server behavior found. A request arriving with a
|
||||
sequence number lower than the highest already seen, or 64 or more ahead of the
|
||||
previous one, causes the reported window to become `0x1` — history is discarded,
|
||||
not merely left unshifted, and it is never recovered.
|
||||
|
||||
```
|
||||
seq 0 -> count 1 window 0x0000000000000001
|
||||
seq 1 -> count 2 window 0x0000000000000003
|
||||
seq 5 -> count 3 window 0x0000000000000031
|
||||
seq 3 -> count 4 window 0x0000000000000001 <- gap fill discards history
|
||||
```
|
||||
|
||||
Duplicates leave the window unchanged and advance the count. 32-bit sequence
|
||||
wraparound is handled as ordinary modular arithmetic. Window value 0 was never
|
||||
observed. Full tables in `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1;
|
||||
capture in `captures/server-recv-window.pcapng`.
|
||||
|
||||
The three-case rule stated in `IRTT_SERVER_PROTOCOL_SPEC.md` Section 10.2 was
|
||||
cross-checked against a live server over 12 sessions of 40 requests each — 480
|
||||
replies — with sequence numbers drawn from a mix of in-order steps, duplicates,
|
||||
forward gaps of 2 to 80 and backward steps of 1 to 5, and starting sequence
|
||||
numbers of 0, 1, 7, 100 and 4294967294. **480 replies checked, 0 mismatches** in
|
||||
either window or count.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 12.8 and 19.15).
|
||||
|
||||
## Finding S-3: Received count semantics
|
||||
|
||||
The count advances by one for every request that reaches the reply stage,
|
||||
including duplicates and out-of-order arrivals, and never for requests dropped
|
||||
for bad magic, bad flags, bad authentication, unknown token, endpoint mismatch,
|
||||
oversize, or rate limiting. It is per session and includes the request being
|
||||
answered. Confirmed against a rate-limited server: 12 back-to-back requests
|
||||
produced 5 replies with counts 1–5, and three later requests continued 6–8.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-4: Server-initiated close exists and is triggered by the duration limit
|
||||
|
||||
Resolves client-spec open question 19.3. A server with a maximum test duration
|
||||
answers the first echo request past its hard deadline with a complete echo reply
|
||||
carrying flags `0x06` (Reply|Close), and removes the session at that moment.
|
||||
|
||||
| Configured maximum duration | Close flag observed at |
|
||||
|---|---|
|
||||
| 1 s | 3.01 s after the first echo |
|
||||
| 2 s | 4.06 s / 4.10 s after the first echo |
|
||||
|
||||
The deadline is the maximum duration plus a 2-second grace, measured from the
|
||||
session's first echo request — not from open. Capture:
|
||||
`captures/server-close.pcapng`, frame 36.
|
||||
|
||||
Driving an upstream 0.9.1 client against a server that sets the flag mid-test:
|
||||
the client discards that reply's measurement, terminates with a "server closed
|
||||
connection" condition, prints partial results, and exits successfully.
|
||||
|
||||
No other condition — idle expiry, shutdown, rate limiting, oversize, auth
|
||||
failure — was observed to set the flag.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 13.7 and 19.3).
|
||||
|
||||
## Finding S-5: Client-initiated close is never acknowledged, and a close reply is harmless
|
||||
|
||||
Upstream 0.9.1 sends **nothing** in response to a close request. Repeated closes
|
||||
and post-close echoes are dropped as unknown-token packets. Capture:
|
||||
`captures/server-close-lifecycle.pcapng` ends with three unanswered client
|
||||
packets.
|
||||
|
||||
A contaminated-side server that *does* reply to close — magic + flags(Reply|Close)
|
||||
+ [MAC] + token — was driven against a real 0.9.1 client and the run completed
|
||||
normally with correct results, with nothing in the client's output changing.
|
||||
Sending such a datagram is therefore harmless, but nothing was observed to make
|
||||
use of it: it has no demonstrated purpose in protocol version 1, and the server
|
||||
specification does not define it as required behavior.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-6: Protocol version is not enforced by the server
|
||||
|
||||
Open requests with `ProtocolVersion` of 0, 2, −1, or the tag omitted entirely,
|
||||
each produced a session-creating open reply (flags `0x03`, non-zero token) with
|
||||
`ProtocolVersion = 1` returned. The client spec's claim that "the server will
|
||||
also set the Close flag if there is a version mismatch" is not true of upstream
|
||||
0.9.1.
|
||||
|
||||
**Classification:** CLIENT SPEC CORRECTION (client spec Sections 6.2, 9.1,
|
||||
19.13).
|
||||
|
||||
## Finding S-7: Dual-field midpoint wire representation
|
||||
|
||||
With StampAt = midpoint and a single negotiated clock, the reply carries both
|
||||
midpoint fields — wall then monotonic — which is one 8-byte field more than the
|
||||
negotiated parameters imply, and makes the datagram longer at small negotiated
|
||||
lengths. Measured across every combination of received-stats and clock; confirmed
|
||||
at the application layer by an upstream client reporting 36-byte packets while
|
||||
receiving 44-byte datagrams and completing normally.
|
||||
|
||||
A contaminated-side server emitting only the negotiated midpoint field was driven
|
||||
by the 0.9.1 client for wall-only, monotonic-only and dual-clock configurations,
|
||||
and the client completed every run cleanly. This is consistency validation, not
|
||||
an independent observation; what it shows is that the *client* accepts the
|
||||
conforming form.
|
||||
|
||||
**Extended 2026-08-11 — see Finding S-18** for the negotiated-length dependence,
|
||||
the version comparison, and the correction to the "always +8" characterisation.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION **and** CLIENT SPEC
|
||||
CLARIFICATION (client spec Sections 8.5 and 19.14).
|
||||
|
||||
## Finding S-8: Interval restriction resolves the client's safety floor
|
||||
|
||||
A server restricts the negotiated interval to at most one quarter of its idle
|
||||
timeout (measured: 4 s → 1 s, 8 s → 2 s, 20 s → 5 s, 40 s → 10 s, 60 s → 15 s).
|
||||
This gives an ordinary, unmodified server that reduces a client's interval, which
|
||||
resolves client-spec open question 19.9.
|
||||
|
||||
| Server idle timeout | Returned interval | Client outcome |
|
||||
|---|---|---|
|
||||
| 2 s | 500 ms | Aborts — strict **and** loose |
|
||||
| 3 s | 750 ms | Aborts — strict **and** loose |
|
||||
| 4 s | 1 s (no reduction) | Proceeds |
|
||||
|
||||
Reduction to exactly 1 s is an ordinary restriction: rejected in strict mode,
|
||||
accepted in loose mode. The safety floor is **1 second**.
|
||||
|
||||
**Classification:** CLIENT SPEC CLARIFICATION (client spec Sections 6.2, 19.9).
|
||||
|
||||
## Finding S-9: Every rejection is a silent drop
|
||||
|
||||
No error, reset, or NACK exists anywhere in the protocol. Verified for: short
|
||||
datagrams, bad magic, reserved flag bits, the Reply flag on an inbound packet,
|
||||
every authentication failure mode, malformed parameters, invalid parameter enum
|
||||
values, unknown tokens, endpoint mismatch, oversize requests and rate limiting.
|
||||
Full table in `IRTT_SERVER_PROTOCOL_SPEC.md` Section 17.
|
||||
|
||||
Minimum accepted request sizes without authentication: open 4 bytes, close
|
||||
12 bytes, echo 16 bytes; add 16 bytes with authentication.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION, CONSISTENT with the client
|
||||
spec's HMAC-drop finding (19.8).
|
||||
|
||||
## Finding S-10: Authentication must be applied to replies, including echo replies
|
||||
|
||||
A contaminated-side server that authenticated its open reply correctly but
|
||||
omitted the HMAC flag from echo replies caused the 0.9.1 client to discard every
|
||||
echo reply and terminate abnormally with zero packets received. Fixing the flag
|
||||
made all configurations pass. The load-bearing observation here is the **client's**
|
||||
reaction, which is black-box; the server was merely the stimulus.
|
||||
|
||||
All authentication failures — missing flag, missing field, wrong key, flipped
|
||||
bit, zeroed MAC, truncated MAC, flag/MAC disagreement in either direction —
|
||||
produce a silent drop with no session state change. A session survived eight
|
||||
consecutive failing probes and served the next valid request normally.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-11: Idle expiry has a 5-second grace, and never-used sessions do not expire
|
||||
|
||||
With a 2-second idle timeout, the boundary was measured between 6.9 s and 7.1 s,
|
||||
i.e. timeout + 5 s. The first request past the deadline is still answered and
|
||||
the session is released while handling it; only the next request is dropped.
|
||||
|
||||
A session that has never carried an echo request does not expire at all: opened
|
||||
and left idle for 9 seconds under a 2-second timeout, it was served normally.
|
||||
Combined with the fact that every open — including a retransmitted one — creates
|
||||
a new session, this means open retransmissions accumulate permanent state.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-12: No session or per-peer limits by default
|
||||
|
||||
1000 sequential opens from a single source endpoint all succeeded with distinct
|
||||
tokens, and the first, middle and last were all still usable afterwards. Two
|
||||
opens from the same source port yield two independently usable sessions.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-13: Tested post-0.9.1 tree — CLI-only difference
|
||||
|
||||
The development tree built for this study is **six upstream commits** past the
|
||||
0.9.1 tag. Those commits are not part of the 0.9.1 release, and nothing in this
|
||||
finding describes 0.9.1. The only functional change among them is an opt-in
|
||||
server ECN option; the remainder are documentation, roadmap and dependency
|
||||
updates.
|
||||
|
||||
- The option's documentation states that it "disables UDP replies from server."
|
||||
**Observed behavior contradicts this**: with the option enabled, sessions
|
||||
completed normally over both IPv4 and IPv6 (4/4 packets, no loss).
|
||||
- Enabling it forces the source-IP-pinning policy.
|
||||
- ECN observation is a receive-side property of the IP header and does not
|
||||
change the IRTT payload in either direction.
|
||||
|
||||
A side-by-side run of both builds with an identical scripted request sequence
|
||||
(open with full statistics and dual timestamps on both clocks; echoes 0, 1, 4,
|
||||
3; close) produced identical structure: same open-reply layout, 60-byte replies,
|
||||
same flags, counts 1–4, windows `0x1`, `0x3`, `0x19`, `0x1`, and no close reply
|
||||
in either case.
|
||||
|
||||
**Classification:** VERSION DIFFERENCE — CLI / server-policy. Not wire-visible.
|
||||
|
||||
## Finding S-14: Reply length is capped by the interface MTU but the negotiated value is not
|
||||
|
||||
On a 1500-byte-MTU interface, negotiated lengths of 1501, 2000, 3000 and 8000
|
||||
each produced 1500-byte replies while the open reply returned the requested
|
||||
value unchanged. Clients treat a short reply as fatal, so such a session opens
|
||||
and then fails on the first echo.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-15: Robustness hazards reachable from ordinary traffic
|
||||
|
||||
Two conditions terminate the upstream server process. Both are recorded in
|
||||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 22 so that a clean implementation does
|
||||
not reproduce them.
|
||||
|
||||
1. **Oversized reply.** If the negotiated length exceeds the host's maximum
|
||||
outbound datagram size, the send fails with a non-transient error, which is
|
||||
treated as fatal: the listener stops and the process exits. Observed on a
|
||||
host with a 9216-byte maximum: a session negotiating 15000 bytes killed the
|
||||
server on its first echo reply. The default configuration places no upper
|
||||
bound on the negotiated length.
|
||||
2. **Zero-length fill pattern.** A server-fill descriptor of the form
|
||||
`pattern:` with an empty hex body terminates the server 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. An unparseable hex body
|
||||
such as `pattern:zz` is handled correctly by falling back to the server
|
||||
default.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION.
|
||||
|
||||
## Finding S-16: Server fill is a continuous stream
|
||||
|
||||
The default fill is the repeating pattern `69 72 74 74`, and its phase is not
|
||||
reset per packet or per session — it advances continuously as bytes are consumed
|
||||
and carries over between sessions on the same listener.
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
Under a no-fill policy the payload contains unspecified residual bytes; one
|
||||
observed reply's trailing bytes were a fragment of a previous packet's parameter
|
||||
payload.
|
||||
|
||||
**Classification:** SERVER-ONLY NEW INFORMATION; also noted as a client-side
|
||||
caution in client spec Section 8.5.
|
||||
|
||||
## Finding S-17: Contaminated-side consistency validation of the model
|
||||
|
||||
A server built from the behavioral model in `IRTT_SERVER_PROTOCOL_SPEC.md` — with
|
||||
no upstream code — was driven by the real 0.9.1 client across: default
|
||||
parameters; no statistics and no timestamps; midpoint with wall, monotonic and
|
||||
both clocks; send-only with wall; receive-only with monotonic; a 200-byte packet
|
||||
length; no-test mode; HMAC with a matching key; HMAC with a mismatched key; and
|
||||
IPv6. Every configuration produced a clean, lossless run with correct results,
|
||||
and the mismatched-key case correctly timed out.
|
||||
|
||||
**This is contaminated-side consistency validation, not independent
|
||||
verification.** The server was written on this side of the clean-room boundary,
|
||||
so it cannot corroborate anything about upstream. What it does show is that the
|
||||
specification as written is sufficient to build a server the real upstream client
|
||||
accepts — the client's acceptance being the black-box part. No statement anywhere
|
||||
in the outgoing documents rests on this exercise.
|
||||
|
||||
**Classification:** Consistency validation, not a finding.
|
||||
|
||||
---
|
||||
|
||||
## Part II Captures Created
|
||||
|
||||
| File | Contents |
|
||||
|------|----------|
|
||||
| `captures/server-recv-window.pcapng` | Sequential, gap, duplicate, reorder and far-gap sequence numbers on one session |
|
||||
| `captures/server-close-lifecycle.pcapng` | Client close, repeated close, post-close echo — none answered |
|
||||
| `captures/server-close.pcapng` | Server-initiated close via the duration limit; frame 36 carries flags `0x06` |
|
||||
| `captures/server-session-identity.pcapng` | Foreign-source-port echo and close, unknown token, original endpoint still served |
|
||||
|
||||
## Part II Vectors Created
|
||||
|
||||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md`.
|
||||
|
||||
## Part II Client Spec Changes Applied
|
||||
|
||||
| Section | Change | Class |
|
||||
|---------|--------|-------|
|
||||
| 2 | Pointer to the companion server specification | Editorial |
|
||||
| 6.2, 9.1, 19.13 | Server does not enforce the protocol version | **CORRECTION** |
|
||||
| 6.2, 19.9 | Interval safety floor pinned to 1 second, enforced in loose mode too | **CLARIFICATION** |
|
||||
| 8.5, 19.14 | The one verified case in which a reply exceeds `compatible_reply_len`, scoped to the single extra length `upstream_0_9_1_reply_len`; `compatible_reply_len` rather than the negotiated Length is the normal minimum; payload content is not predictable | **CLARIFICATION** |
|
||||
| 12.8, 19.15 | A window of `0x1` carries no history; window 0 is never emitted | **CLARIFICATION** |
|
||||
| 13.7, 19.3 | Server-initiated close confirmed; observed client reaction | **CLARIFICATION** |
|
||||
| 19.10 | No protocol limit on received parameter payload size | **CLARIFICATION** |
|
||||
|
||||
## Part II Summary
|
||||
|
||||
- Resolved client-spec open questions: **19.3**, **19.9**, **19.10**.
|
||||
- New client-spec open questions resolved on discovery: **19.13**, **19.14**,
|
||||
**19.15**.
|
||||
- Still unresolved on the client side: **19.6** (not externally testable),
|
||||
**19.12** (hard to trigger).
|
||||
- Server-side unknowns are enumerated in `IRTT_SERVER_PROTOCOL_SPEC.md`
|
||||
Section 23.
|
||||
|
||||
---
|
||||
|
||||
# Part III — Midpoint Timestamp Follow-Up (2026-08-11)
|
||||
|
||||
A focused follow-up on Finding S-7. Three server builds were exercised with a raw
|
||||
UDP harness that performs the open negotiation, sends one echo request, and
|
||||
reports the exact reply datagram length and the bytes at the timestamp offsets:
|
||||
|
||||
| Build | Provenance |
|
||||
|-------|-----------|
|
||||
| **0.9.1** | the upstream release; the baseline for this study |
|
||||
| **0.9.0** | the preceding upstream release, for comparison |
|
||||
| **post-0.9.1 tree** | the upstream development tree six upstream commits past the 0.9.1 tag — post-release work, not part of any release |
|
||||
|
||||
All on macOS arm64, loopback.
|
||||
|
||||
**Provenance note.** The measurements below are black-box observations. Part III
|
||||
additionally records one **source-assisted historical conclusion** — a judgement
|
||||
about whether the measured 0.9.0/0.9.1 difference was intended — which is
|
||||
labelled where it appears and is **not** presented as independently verified.
|
||||
Nothing normative in the specifications depends on it, and the detailed
|
||||
contaminated-side evidence behind it was retained on the contaminated side and
|
||||
does not appear here.
|
||||
|
||||
## Finding S-18: Dual-field midpoint emission is length-dependent in effect
|
||||
|
||||
### The wire representation
|
||||
|
||||
For StampAt = midpoint, upstream 0.9.1 emits two 8-byte timestamp fields — wall
|
||||
first, monotonic second — for every negotiated Clock, occupying a fixed 16-byte
|
||||
region. Full StampAt × Clock matrix at negotiated length 0, received-stats =
|
||||
both, against 0.9.1:
|
||||
|
||||
| StampAt | Clock = wall | Clock = monotonic | Clock = both |
|
||||
|---------|--------------|-------------------|--------------|
|
||||
| none | 28 | 28 | 28 |
|
||||
| send | 36 | 36 | 44 |
|
||||
| receive | 36 | 36 | 44 |
|
||||
| both | 44 | 44 | 60 |
|
||||
| **midpoint** | **44** (expected 36) | **44** (expected 36) | 44 |
|
||||
|
||||
Every non-midpoint row matches the negotiated layout exactly. Only midpoint
|
||||
deviates, and only when a single clock is negotiated.
|
||||
|
||||
### The length dependence — correction to "always +8"
|
||||
|
||||
Reply length is the larger of the negotiated length and the field block; the
|
||||
extra field enlarges only the field block. Writing `normal_header` for the block
|
||||
the negotiated parameters imply and `upstream_header` for that block plus one
|
||||
8-byte timestamp field:
|
||||
|
||||
```
|
||||
compatible_reply_len = max(negotiated_length, normal_header)
|
||||
upstream_0_9_1_reply_len = max(negotiated_length, upstream_header)
|
||||
```
|
||||
|
||||
Negotiated-length sweep, 0.9.1, midpoint + wall + received-stats = both
|
||||
(`normal_header` 36, `upstream_header` 44):
|
||||
|
||||
| Negotiated length | 0 | 16 | 32 | 36 | 37 | 40 | 43 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Reply datagram | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 44 | 45 | 48 | 64 | 128 | 1024 | 4096 |
|
||||
| Excess over a compatible server | +8 | +8 | +8 | +8 | +7 | +4 | +1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|
||||
|
||||
Repeated for received-stats = none, count and window; the two minima shift to
|
||||
24/32, 28/36 and 32/40 and the pattern is identical. The dual-clock control case
|
||||
(midpoint + both clocks) showed zero excess at every length.
|
||||
|
||||
At negotiated length 4096 the midpoint region still contains both fields,
|
||||
followed by fill bytes — so the emission itself is unconditional; only its effect
|
||||
on the datagram length depends on the negotiated length.
|
||||
|
||||
**This corrects the earlier characterisation.** "Upstream replies are the
|
||||
negotiated length plus 8 bytes", or "the normal packet length plus 8", is true
|
||||
only while the negotiated length is at or below `normal_header`. Between the two
|
||||
minima the excess falls through +7…+1, and once the negotiated length reaches
|
||||
`upstream_header` the excess is **0** — the reply is exactly the negotiated
|
||||
length and the extra field displaces payload. Any surviving "+8 always" wording
|
||||
elsewhere is stale and wrong.
|
||||
|
||||
**And it is a per-negotiation figure.** Each column of the sweep is a distinct
|
||||
negotiation whose upstream reply has exactly one length,
|
||||
`upstream_0_9_1_reply_len`. The +8 / +7…+1 / 0 values are what the difference
|
||||
works out to across those different negotiations. No negotiation produced a range
|
||||
of lengths, and no intermediate length between `compatible_reply_len` and
|
||||
`upstream_0_9_1_reply_len` was ever observed. Wording that reads as "any reply up
|
||||
to 8 bytes over is acceptable" is equally stale and wrong.
|
||||
|
||||
### Reachable without requesting midpoint
|
||||
|
||||
Against a 0.9.1 server restricted to a single timestamp, a request for StampAt =
|
||||
both is answered with StampAt = midpoint (already recorded in the timestamp
|
||||
allowance table). Measured: requested `3/wall` → negotiated `4/wall`, reply 44
|
||||
where 36 was implied; requested `3/monotonic` → same; requested `3/both` →
|
||||
negotiated `4/both`, reply 44, no excess.
|
||||
|
||||
### Value consequences for a positional decoder
|
||||
|
||||
With Clock = monotonic the first 8 bytes of the midpoint region hold the wall
|
||||
value, so a decoder that expects one midpoint field reads a
|
||||
nanoseconds-since-epoch magnitude into its monotonic slot. Measured with the
|
||||
upstream 0.9.1 client itself (`--tstamp=midpoint --clock=monotonic`): reported
|
||||
server monotonic timestamps of 1786437872024631833 and 1786437872225745750 ns —
|
||||
about 56.6 years, not process uptime — with the run completing normally and
|
||||
reporting 72 bytes sent against 88 bytes received for two packets.
|
||||
|
||||
**Limit of what this supports.** The misread is verified; a general correction is
|
||||
not. Once the negotiated length reaches `upstream_header` the two forms are the
|
||||
same size:
|
||||
|
||||
```
|
||||
conforming single-clock: [mono][payload...]
|
||||
upstream 0.9.1: [wall][mono][payload...]
|
||||
```
|
||||
|
||||
and nothing in the datagram tells them apart. A client that unconditionally read
|
||||
the second 8-byte region as monotonic would, against a conforming server, be
|
||||
reading ordinary payload. The specifications therefore permit a correction only
|
||||
where the dual-field form is otherwise identifiable, and record the equal-length
|
||||
case as ambiguous rather than inventing a heuristic for it.
|
||||
|
||||
### Version comparison — black-box measured
|
||||
|
||||
Same harness, same parameters, negotiated length 0, received-stats = both:
|
||||
|
||||
| Build | midpoint + wall | midpoint + monotonic | midpoint + 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 |
|
||||
| tested post-0.9.1 tree | 44 bytes, two fields | 44 bytes, two fields | 44 bytes, two fields |
|
||||
|
||||
0.9.0 emits exactly the negotiated fields; 0.9.1 emits the dual-field form; the
|
||||
tested post-0.9.1 tree behaves like 0.9.1. The full matrix and the length sweep
|
||||
were re-run against the post-0.9.1 build and matched 0.9.1 in every cell.
|
||||
|
||||
This is a measurement. It says which bytes each build put on the wire, and
|
||||
nothing about why.
|
||||
|
||||
### Deliberate or defect — source-assisted historical conclusion
|
||||
|
||||
**Labelled contaminated-side judgement. Not an independent observation, and not
|
||||
load-bearing.** Contaminated-side investigation of upstream's release history
|
||||
indicates that the 0.9.0-to-0.9.1 difference measured above is an **unintended
|
||||
regression** rather than a designed protocol feature. Nothing in upstream's
|
||||
published release notes or documentation refers to the behavior, and none of the
|
||||
six post-0.9.1 upstream commits changes it.
|
||||
|
||||
Three things about this paragraph:
|
||||
|
||||
1. The measured 0.9.0-versus-0.9.1 difference is stated separately, above, and
|
||||
stands entirely on its own.
|
||||
2. That measurement is *consistent with* this judgement but does not establish
|
||||
it. Intent is not observable from the wire.
|
||||
3. The contaminated-side evidence supporting the judgement was retained on the
|
||||
contaminated side and deliberately did not cross the boundary. Only the
|
||||
conclusion appears here.
|
||||
|
||||
No requirement in either specification depends on this paragraph. A reader who
|
||||
disregards it loses nothing normative.
|
||||
|
||||
**Classification:** SERVER SPEC CORRECTION (server spec Sections 9.2, 11.3.1,
|
||||
21.1, 21.2, 23.4, 24) **and** CLIENT SPEC CORRECTION (client spec Sections 8.5,
|
||||
19.14).
|
||||
Reference in new issue
Block a user