1763 lines
75 KiB
Markdown
1763 lines
75 KiB
Markdown
# IRTT-Compatible Client Protocol Specification
|
||
|
||
- **Version:** 1.3-verified
|
||
- **Date:** 2026-08-11
|
||
- **Verified against:** irtt 0.9.1 release, protocol version 1, macOS Darwin
|
||
25.3.0 / 25.5.0 arm64
|
||
|
||
**Changes in 1.2:** corrections and clarifications arising from server-side
|
||
verification. See Sections 19.3, 19.9, 19.10, 19.13, 19.14 and 19.15, and the
|
||
companion `IRTT_SERVER_PROTOCOL_SPEC.md`.
|
||
|
||
**Changes in 1.3:** the reply-length tolerance of Sections 8.5 and 19.14 is
|
||
narrowed from "accept any longer reply" to the one verified single-clock midpoint
|
||
case, and stated as a single additional accepted length —
|
||
`upstream_0_9_1_reply_len` exactly — rather than a `+1..+8` range; the normal
|
||
minimum and expected reply size is stated as `compatible_reply_len` rather than
|
||
the negotiated `Length` value alone; the monotonic-only decoding ambiguity is
|
||
recorded rather than papered over with a heuristic; the DSCP wire encoding is
|
||
separated from user-facing notation (Section 10.8); and the stale suggestion that
|
||
servers reject a protocol-version mismatch is removed (Section 6.2).
|
||
|
||
---
|
||
|
||
## 1. Status
|
||
|
||
This document describes the externally observable behavior required for an
|
||
independent client implementation compatible with existing IRTT (Isochronous
|
||
Round-Trip Tester) servers.
|
||
|
||
This document does **not** describe upstream source structure, internal
|
||
algorithms, 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" in this document are to be interpreted
|
||
as interoperability requirements, consistent with RFC 2119.
|
||
|
||
---
|
||
|
||
## 2. Scope
|
||
|
||
- **Client-only implementation.** Server implementation is out of scope unless
|
||
server behavior is needed to understand client interoperability. Server
|
||
behavior is specified separately in `IRTT_SERVER_PROTOCOL_SPEC.md`, which
|
||
shares this document's wire format.
|
||
- **Compatible with existing IRTT servers** running protocol version 1.
|
||
- This specification describes protocol and measurement behavior; CLI design
|
||
is not prescribed.
|
||
|
||
---
|
||
|
||
## 3. Terminology
|
||
|
||
| Term | Definition |
|
||
|------|-----------|
|
||
| **Client** | The endpoint that initiates a session, sends test packets, and receives replies. |
|
||
| **Server** | The endpoint that accepts sessions and echoes test packets back to the client. |
|
||
| **Session** | A stateful association between a client and server, identified by a connection token. |
|
||
| **Open / Negotiation phase** | The initial handshake in which the client proposes test parameters and the server responds with (possibly restricted) parameters and a connection token. |
|
||
| **Test packet** | A UDP datagram sent by the client during the active test phase. Sometimes called an "echo request." |
|
||
| **Reply packet** | A UDP datagram sent by the server in response to a test packet. Sometimes called an "echo reply." |
|
||
| **Close / Finalization phase** | The exchange in which the client notifies the server that the session is ending. |
|
||
| **Connection token** | A 64-bit value assigned by the server during negotiation, used to identify the session in all subsequent packets. |
|
||
| **Round-trip time (RTT)** | The time between sending a test packet and receiving its reply, optionally minus server processing time. |
|
||
| **Send interval** | The fixed time period between successive test packet transmissions. |
|
||
| **Sequence number** | A 32-bit unsigned integer, starting at 0, assigned to each test packet in order of transmission. |
|
||
| **Timeout** | The duration the client waits for a server reply during the open phase before retransmitting. |
|
||
| **Packet loss** | A test packet for which no reply was received. |
|
||
| **Duplicate** | A reply received with a sequence number for which a reply was already recorded. |
|
||
| **Late packet** | A reply received with a sequence number lower than the most recently received sequence number (indicates out-of-order delivery). |
|
||
| **IPDV (Instantaneous Packet Delay Variation)** | The difference in delay between two successive successfully returned replies. Commonly called "jitter." |
|
||
| **Server processing delay** | The time the server spent between receiving a request and sending its reply, measured by server timestamps. |
|
||
| **Received count** | A running count of all packets the server has received for this session. |
|
||
| **Received window** | A 64-bit bitmap indicating the receipt status of the most recent 64 packets at the server. |
|
||
| **Wall clock** | System real-time clock (nanoseconds since Unix epoch). Subject to NTP adjustments. |
|
||
| **Monotonic clock** | A clock that only moves forward and is not subject to system time adjustments. Used for duration calculations. |
|
||
| **HMAC** | Hash-based Message Authentication Code (MD5), used for packet authentication. |
|
||
| **DSCP** | Differentiated Services Code Point, set in the IP header TOS/Traffic Class field. |
|
||
| **Drain period** | The period after the last test packet is sent during which the client waits for outstanding replies. |
|
||
|
||
---
|
||
|
||
## 4. Conformance
|
||
|
||
A conforming client MUST implement:
|
||
|
||
1. Session lifecycle: open, test, close.
|
||
2. The binary wire format defined in this document.
|
||
3. Parameter negotiation.
|
||
4. Connection token handling.
|
||
5. Sequence number assignment and reply matching.
|
||
6. RTT measurement.
|
||
7. Packet loss detection.
|
||
8. Tolerance for UDP reordering and duplication.
|
||
|
||
A conforming client SHOULD implement:
|
||
|
||
1. HMAC authentication.
|
||
2. Server timestamp handling (one-way delay, IPDV).
|
||
3. Received stats (count and window) processing.
|
||
4. Configurable DSCP.
|
||
|
||
Implementations are not required to use any specific internal architecture,
|
||
data structure, function layout, naming convention, concurrency model, or
|
||
source organization. Only the externally observable behavior described in
|
||
this document is normative.
|
||
|
||
---
|
||
|
||
## 5. Transport Requirements
|
||
|
||
### 5.1 Transport Protocol
|
||
|
||
IRTT uses **UDP** exclusively. There is no TCP fallback.
|
||
|
||
### 5.2 Default Port
|
||
|
||
The default server port is **2112**. The default client local port is
|
||
ephemeral (OS-assigned, port 0).
|
||
|
||
### 5.3 IPv4 and IPv6
|
||
|
||
A conforming client MUST support IPv4. IPv6 support is RECOMMENDED. When the
|
||
user does not specify an IP version preference, the client SHOULD attempt
|
||
address resolution for both IPv4 and IPv6, and use whichever resolves
|
||
successfully. If both resolve, the selection behavior is
|
||
implementation-defined.
|
||
|
||
The client MUST use a single connected UDP socket for the entire session. The
|
||
client calls the equivalent of `connect()` on the socket, binding it to the
|
||
resolved server address before the open phase.
|
||
|
||
### 5.4 Socket Options
|
||
|
||
- **DSCP/TOS:** A client SHOULD support setting the DSCP value on outgoing
|
||
packets. For IPv4, this is the TOS field. For IPv6, this is the Traffic
|
||
Class field. The DSCP value is negotiated as a parameter; if the server
|
||
disallows DSCP, the negotiated value will be 0.
|
||
- **TTL/Hop Limit:** A client MAY support setting the IP TTL (IPv4) or Hop
|
||
Limit (IPv6).
|
||
- **Don't Fragment (DF):** A client MAY support setting the DF bit.
|
||
- **ECN:** ECN bit handling is OPTIONAL and experimental.
|
||
|
||
### 5.5 Connectionless Behavior
|
||
|
||
Although the client uses a connected UDP socket, the protocol is
|
||
connectionless at the network layer. The client MUST tolerate:
|
||
|
||
- Packet reordering.
|
||
- Packet duplication.
|
||
- Packet loss.
|
||
- Receiving unrelated UDP traffic on the socket (which MUST be discarded
|
||
based on validation rules).
|
||
|
||
### 5.6 MTU Considerations
|
||
|
||
Common maximum unfragmented UDP payload sizes for a 1500-byte MTU:
|
||
- IPv4: 1472 bytes (1500 - 20 IPv4 header - 8 UDP header)
|
||
- IPv6: 1452 bytes (1500 - 40 IPv6 header - 8 UDP header)
|
||
|
||
The client does not need to perform path MTU discovery. Packet length is a
|
||
user-configured parameter.
|
||
|
||
---
|
||
|
||
## 6. Session Lifecycle
|
||
|
||
A client session progresses through the following externally observable states:
|
||
|
||
```
|
||
Resolving ──► Opening ──► Active Test ──► Draining ──► Closing ──► Completed
|
||
│ │
|
||
▼ ▼
|
||
Failed Completed
|
||
```
|
||
|
||
### 6.1 Resolving
|
||
|
||
- **Entry:** Client is given a server address (hostname or IP) and port.
|
||
- **Behavior:** The client resolves the address to a UDP endpoint. The
|
||
default port (2112) MUST be appended if no port is specified.
|
||
- **Exit:** Successful resolution transitions to Opening. Failure transitions
|
||
to Failed.
|
||
|
||
### 6.2 Opening / Negotiating
|
||
|
||
- **Entry:** The client has a resolved server address and a connected UDP socket.
|
||
- **Behavior:**
|
||
1. The client constructs an open request packet (see Section 8.2) containing
|
||
the proposed test parameters serialized in the payload.
|
||
2. The client sends the open request and waits for a reply.
|
||
3. If no reply is received within the current timeout, the client
|
||
retransmits the open request with the next timeout value from its
|
||
timeout list.
|
||
4. The default timeout sequence is: 1s, 2s, 4s, 8s (total maximum wait:
|
||
15s, maximum 4 packets). The minimum timeout value MUST be at least
|
||
200ms.
|
||
5. If all timeouts are exhausted with no reply, the session transitions to
|
||
Failed.
|
||
|
||
- **Valid server responses:**
|
||
- An open reply with a non-zero connection token: session parameters are
|
||
extracted from the reply payload. The client stores the connection token.
|
||
Transitions to Active Test (or Completed if the no-test flag was set).
|
||
- An open reply with the close flag set, **when the client did NOT set
|
||
Close in its request**: the server rejected the session. Transition to
|
||
Failed. Note that upstream 0.9.1 servers were never observed to produce
|
||
such a rejection — in particular they do **not** reject a mismatched
|
||
protocol version this way (see below and Section 19.13) — so a client MUST
|
||
handle the shape without depending on any server actually emitting it.
|
||
- An open reply with the close flag set, **when the client DID set Close
|
||
in its request** (no-test mode): the server acknowledged the open+close.
|
||
The connection token is zero. Transition to Completed.
|
||
[**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding D.]
|
||
|
||
- **Parameter negotiation:** The server MAY restrict parameters. The client
|
||
MUST compare returned parameters against its requested parameters. A
|
||
conforming client MUST, by default, reject the session if parameters were
|
||
restricted (transition to Failed with an appropriate error). A "loose" mode
|
||
MAY be provided that accepts server restrictions with a warning.
|
||
|
||
The client MUST reject (as an error) the following server parameter changes:
|
||
- Duration increased beyond what was requested.
|
||
- Length increased beyond what was requested.
|
||
- Interval reduced below a safety floor of **1 second**. This rejection
|
||
applies in loose mode as well as strict mode.
|
||
[**Verified 2026-08-11** — see Section 19.9.]
|
||
|
||
- **Protocol version check:** The client MUST verify that the server's
|
||
protocol version matches exactly. Protocol version 1 is the current
|
||
version. Mismatch MUST cause the session to fail.
|
||
|
||
The client cannot rely on the server to detect the mismatch. Upstream 0.9.1
|
||
servers do **not** reject a mismatched protocol version: they rewrite it to 1,
|
||
open a normal session, and return `ProtocolVersion = 1` with the Close flag
|
||
clear. Version checking is therefore effectively client-side only.
|
||
[**Verified 2026-08-11** — see Section 19.13 and
|
||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 7.5.]
|
||
|
||
### 6.3 Active Test
|
||
|
||
- **Entry:** Open reply received and parameters accepted.
|
||
- **Behavior:**
|
||
1. The client sends test packets at fixed intervals for the configured
|
||
duration.
|
||
2. The client MUST be able to receive reply packets while continuing
|
||
to send test packets (i.e., sending and receiving occur concurrently).
|
||
3. Test packets are numbered with sequence numbers starting at 0.
|
||
4. The test duration is **exclusive**: the client MUST NOT send a packet at
|
||
or after the end time. The end time is calculated as start time + duration.
|
||
The next scheduled send time is computed, and if it would be at or past
|
||
the end time, sending stops.
|
||
5. The expected number of packets for a given duration `d` and interval `i`
|
||
is: `ceil(d / i)`. [**Verified 2026-04-28** by black-box testing with
|
||
irtt 0.9.1. See BLACKBOX_VERIFICATION_REPORT.md Finding A.]
|
||
|
||
- **Exit:** All scheduled packets have been sent. Transition to Draining.
|
||
|
||
### 6.4 Draining Replies
|
||
|
||
- **Entry:** All test packets have been sent.
|
||
- **Behavior:** The client waits for outstanding replies. The wait duration
|
||
is configurable:
|
||
- Default: 3 times the maximum observed RTT, falling back to 4 seconds if
|
||
no RTT has been measured.
|
||
- Alternatives: a factor of mean RTT, or a fixed duration.
|
||
- If all replies have already been received, no wait is needed.
|
||
|
||
- **Exit:** Wait period expires, or all replies received. Transition to Closing.
|
||
|
||
### 6.5 Closing
|
||
|
||
- **Entry:** Test and drain are complete.
|
||
- **Behavior:** The client sends a single close packet (see Section 8.4) to
|
||
the server. The close packet includes the connection token and the close
|
||
flag. The client does NOT wait for a close acknowledgement (this is a
|
||
known limitation of protocol version 1).
|
||
|
||
- **Exit:** Close packet sent. Transition to Completed.
|
||
|
||
### 6.6 Completed
|
||
|
||
- **Entry:** Session has finished normally.
|
||
- **Behavior:** Results are computed and made available.
|
||
|
||
### 6.7 Failed
|
||
|
||
- **Entry:** An unrecoverable error occurred.
|
||
- **Behavior:** Partial results MAY be available if the failure occurred
|
||
during or after the Active Test phase.
|
||
|
||
---
|
||
|
||
## 7. Packet Format Overview
|
||
|
||
All IRTT packets share a common header structure. The presence or absence of
|
||
optional fields is determined by context (packet type and negotiated
|
||
parameters).
|
||
|
||
| Packet Type | Direction | When Sent | Required | Purpose |
|
||
|------------|-----------|-----------|----------|---------|
|
||
| Open Request | Client → Server | Opening phase | Yes | Propose session parameters |
|
||
| Open Reply | Server → Client | Opening phase | Yes | Return connection token and (possibly restricted) parameters |
|
||
| Echo Request (Test Packet) | Client → Server | Active test | Yes | Carry test payload with sequence number |
|
||
| Echo Reply | Server → Client | Active test | Yes | Return test payload with timestamps and stats |
|
||
| Close Request | Client → Server | Closing phase | Yes | Signal session end |
|
||
| Close Reply | Server → Client | Closing phase | No | Not part of normal operation. Upstream servers send nothing in response to a close; such a datagram is tolerated by clients but has no demonstrated purpose in protocol version 1 |
|
||
|
||
---
|
||
|
||
## 8. Binary Wire Format
|
||
|
||
### 8.1 General Header Structure
|
||
|
||
All multi-byte integer fields use **little-endian** byte order.
|
||
|
||
The packet format uses a flexible field layout. Fields are placed sequentially
|
||
in the packet buffer. A field is either present at its full capacity or absent
|
||
(zero length). The set of present fields is determined by the packet type and
|
||
negotiated parameters.
|
||
|
||
#### 8.1.1 Fixed Header Fields (always present)
|
||
|
||
| Offset | Size (bytes) | Field | Encoding | Description |
|
||
|--------|-------------|-------|----------|-------------|
|
||
| 0 | 3 | Magic | Raw bytes | Fixed value: `0x14 0xA7 0x5B` |
|
||
| 3 | 1 | Flags | Unsigned byte | Bit flags (see below) |
|
||
|
||
**Total minimum header: 4 bytes.**
|
||
|
||
#### 8.1.2 Flag Bits
|
||
|
||
| Bit | Mask | Name | Meaning |
|
||
|-----|------|------|---------|
|
||
| 0 | 0x01 | Open | Set on open request and open reply packets |
|
||
| 1 | 0x02 | Reply | Set on all packets from server to client; clear on all packets from client to server |
|
||
| 2 | 0x04 | Close | Set on close request and close reply packets |
|
||
| 3 | 0x08 | HMAC | Set if an HMAC field is present in the packet |
|
||
|
||
Bits 4-7 are reserved and MUST be zero. A client MUST reject packets where
|
||
reserved flag bits are set.
|
||
|
||
#### 8.1.3 Optional Fields
|
||
|
||
After the fixed header, optional fields appear **in the following order** when
|
||
present. A field is either present at full size or entirely absent. There are
|
||
no length-prefix or presence-indicator bytes; the receiver knows which fields
|
||
are present based on the packet type and negotiated session parameters.
|
||
|
||
**Verification note:** The field ordering below is critical for
|
||
interoperability. [**Verified 2026-04-28** by packet capture with multiple
|
||
field combinations — see BLACKBOX_VERIFICATION_REPORT.md Section 19.11.]
|
||
|
||
| Order | Field | Size (bytes) | Encoding | When Present |
|
||
|-------|-------|-------------|----------|-------------|
|
||
| 1 | HMAC | 16 | MD5 HMAC | When HMAC flag (0x08) is set |
|
||
| 2 | Connection Token | 8 | Little-endian uint64 | All packets except initial open request sent from client before token is known. Present in open reply and all subsequent packets. |
|
||
| 3 | Sequence Number | 4 | Little-endian uint32 | Echo request and echo reply packets |
|
||
| 4 | Received Count | 4 | Little-endian uint32 | Echo reply, when "count" stats negotiated |
|
||
| 5 | Received Window | 8 | Little-endian uint64 | Echo reply, when "window" stats negotiated |
|
||
| 6 | Receive Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when receive timestamps negotiated with wall clock |
|
||
| 7 | Receive Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when receive timestamps negotiated with monotonic clock |
|
||
| 8 | Midpoint Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when midpoint timestamp negotiated with wall clock |
|
||
| 9 | Midpoint Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when midpoint timestamp negotiated with monotonic clock |
|
||
| 10 | Send Wall Timestamp | 8 | Little-endian int64 (ns since Unix epoch) | Echo reply, when send timestamps negotiated with wall clock |
|
||
| 11 | Send Mono Timestamp | 8 | Little-endian int64 (ns, duration) | Echo reply, when send timestamps negotiated with monotonic clock |
|
||
|
||
**Important:** Midpoint timestamps are **mutually exclusive** with
|
||
receive/send timestamps. If a midpoint timestamp is present, receive and send
|
||
timestamp fields MUST NOT be present, and vice versa. A packet containing
|
||
both midpoint and receive/send timestamps is malformed.
|
||
|
||
When both receive and send timestamps are present, they MUST use the same
|
||
clock mode (both wall, both monotonic, or both wall+monotonic).
|
||
|
||
#### 8.1.4 Payload
|
||
|
||
Everything after the last present header field is **payload**. The total
|
||
packet length is the negotiated packet length. If the negotiated length is
|
||
larger than the header, the remaining bytes are payload. If the negotiated
|
||
length is 0 or unspecified, the packet length equals the header length (no
|
||
payload).
|
||
|
||
The client fills the payload with zeros, random data, or a repeating
|
||
pattern, as configured. The server fills reply payloads according to its
|
||
configuration and the negotiated server fill parameter.
|
||
|
||
### 8.2 Open Request Packet
|
||
|
||
**Direction:** Client → Server
|
||
**Flags:** Open (0x01) set. Reply (0x02) clear. HMAC (0x08) set if HMAC key
|
||
configured.
|
||
|
||
**Header fields present:**
|
||
- Magic (3 bytes)
|
||
- Flags (1 byte)
|
||
- HMAC (16 bytes) — if HMAC configured
|
||
|
||
The connection token field is NOT present in the open request (the token has
|
||
not yet been assigned). [**Verified 2026-04-28** by packet capture — see
|
||
BLACKBOX_VERIFICATION_REPORT.md Section 19.1.]
|
||
|
||
**Payload:** Serialized parameters (see Section 8.6).
|
||
|
||
If the client wants to open and immediately close (no-test mode), both the
|
||
Open (0x01) and Close (0x04) flags are set.
|
||
|
||
### 8.3 Open Reply Packet
|
||
|
||
**Direction:** Server → Client
|
||
**Flags:** Open (0x01) set. Reply (0x02) set. Close (0x04) set if the server
|
||
is rejecting the connection. HMAC (0x08) set if applicable.
|
||
|
||
**Header fields present:**
|
||
- Magic (3 bytes)
|
||
- Flags (1 byte)
|
||
- HMAC (16 bytes) — if applicable
|
||
- Connection Token (8 bytes)
|
||
|
||
**Payload:** Serialized parameters (see Section 8.6), reflecting any server
|
||
restrictions.
|
||
|
||
**Validation:**
|
||
- If the Close flag is NOT set, the connection token MUST be non-zero. A
|
||
zero token without the close flag is an error.
|
||
- If the Close flag IS set **and the client did NOT set Close in its open
|
||
request**, the server is rejecting the session (transition to Failed).
|
||
- If the Close flag IS set **and the client DID set Close in its open
|
||
request** (no-test mode), the server acknowledged the open+close. The
|
||
connection token is zero. Transition to Completed.
|
||
[**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding D.]
|
||
|
||
### 8.4 Echo Request Packet (Test Packet)
|
||
|
||
**Direction:** Client → Server
|
||
**Flags:** Reply (0x02) clear. HMAC (0x08) if applicable. Open (0x01) and
|
||
Close (0x04) clear.
|
||
|
||
**Header fields present:**
|
||
- Magic (3 bytes)
|
||
- Flags (1 byte)
|
||
- HMAC (16 bytes) — if applicable
|
||
- Connection Token (8 bytes)
|
||
- Sequence Number (4 bytes)
|
||
- Received Count (4 bytes, zeroed) — if "count" stats negotiated
|
||
- Received Window (8 bytes, zeroed) — if "window" stats negotiated
|
||
- Timestamp fields (zeroed) — according to negotiated stamp_at and clock
|
||
|
||
The echo request includes zeroed placeholder fields for received stats and
|
||
timestamps so that the total packet reaches the negotiated length.
|
||
|
||
**Payload:** Filled according to client configuration (zeros, random, or
|
||
pattern).
|
||
|
||
**Total packet length:** The negotiated `length` parameter. If the header
|
||
(including zeroed optional fields) is larger than the negotiated length, the
|
||
actual packet length is the header length. The final negotiated length
|
||
reflects this minimum.
|
||
|
||
### 8.5 Echo Reply Packet
|
||
|
||
**Direction:** Server → Client
|
||
**Flags:** Reply (0x02) set. HMAC (0x08) if applicable. Close (0x04) MAY be
|
||
set (see Section 19.3 — not yet verified by black-box testing).
|
||
|
||
**Header fields present:**
|
||
- Magic (3 bytes)
|
||
- Flags (1 byte)
|
||
- HMAC (16 bytes) — if applicable
|
||
- Connection Token (8 bytes)
|
||
- Sequence Number (4 bytes)
|
||
- Received Count (4 bytes) — if negotiated, contains server's running count
|
||
- Received Window (8 bytes) — if negotiated, contains 64-bit received bitmap
|
||
- Timestamp fields — according to negotiated stamp_at and clock settings
|
||
|
||
**Payload:** Server-filled according to negotiation. The client MUST NOT rely on
|
||
the payload's content: server fill is applied as a continuous stream, so a reply
|
||
payload does not necessarily begin at the start of the configured fill pattern.
|
||
|
||
**Reply length:** The normal, expected size of an echo reply is
|
||
`compatible_reply_len` — the larger of the negotiated Length and the mandatory
|
||
field block, defined below. That, and not the negotiated `Length` value on its
|
||
own, is the minimum: the client MUST reject a reply *shorter* than
|
||
`compatible_reply_len`. (The two differ whenever the negotiated Length is smaller
|
||
than the field block, including the common case of a negotiated Length of 0.)
|
||
There is exactly one verified case in which a reply is *longer*, and the required
|
||
tolerance is scoped to it — see below. This is **not** a general rule that
|
||
overlong replies are acceptable; outside that one case, a client MUST keep
|
||
applying its ordinary strict validation to unexpectedly sized datagrams.
|
||
|
||
**The one case: dual-field midpoint from upstream 0.9.1.** When midpoint
|
||
timestamps are negotiated, upstream 0.9.1 servers emit **two** midpoint timestamp
|
||
fields on the wire — the midpoint wall field followed by the midpoint monotonic
|
||
field, in that order — **even when the negotiated clock selects only one clock**.
|
||
A single-clock midpoint reply therefore carries one 8-byte field more than the
|
||
negotiated parameters imply. (This describes bytes on the wire only; it implies
|
||
nothing about how any server represents a midpoint internally.)
|
||
|
||
The extra field enlarges the reply's *minimum* size; it does **not** add 8 bytes
|
||
to every reply. Writing `normal_header` for the field block the negotiated
|
||
parameters imply and `upstream_header` for that block plus one 8-byte timestamp
|
||
field:
|
||
|
||
```
|
||
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)
|
||
```
|
||
|
||
Three distinct quantities, not to be conflated: `negotiated_length` is the value
|
||
of the negotiated Length parameter; `normal_header` is the negotiated mandatory
|
||
field block; `compatible_reply_len` is the actual normal packet length implied by
|
||
both.
|
||
|
||
So the excess is +8 while the negotiated length is at or below `normal_header`,
|
||
shrinks through +7…+1 between the two minima, and is **0** once the negotiated
|
||
length reaches `upstream_header` — there the extra field displaces 8 bytes of
|
||
payload instead of lengthening the datagram. Those figures are values the
|
||
difference takes **across different negotiations**. For any one set of negotiated
|
||
Params both quantities are fixed numbers, and the upstream reply has exactly one
|
||
length.
|
||
|
||
**Required tolerance.** When the negotiated parameters are StampAt = Midpoint
|
||
with a single Clock, a conforming client 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 MUST NOT treat its presence as an error.
|
||
|
||
Where `upstream_0_9_1_reply_len > compatible_reply_len`, that form is identifiable
|
||
by length alone and the difference between the two values is between 1 and 8
|
||
bytes. This is one extra accepted length, **not** a `+1..+8` window: a client MUST
|
||
NOT accept an arbitrary length in that span, and MUST reject any length strictly
|
||
between `compatible_reply_len` and `upstream_0_9_1_reply_len` just as it rejects
|
||
any other unexpected length.
|
||
|
||
**Accepted echo reply lengths, in full.** Compute both values once from the
|
||
negotiated Params. A conforming client then accepts exactly these lengths:
|
||
|
||
| 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, and no
|
||
generic monotonic correction is applied (see below).
|
||
|
||
**Values read by a positional decoder.** The extra field is last, and field
|
||
offsets are derived from the negotiated parameters:
|
||
|
||
- **Clock = wall:** the negotiated field comes first, so the value read is
|
||
correct.
|
||
- **Clock = monotonic:** the first 8-byte region holds the *wall*-domain value, so
|
||
an ordinary positional decoder reads a nanoseconds-since-epoch magnitude into
|
||
its monotonic slot instead of a process-relative one.
|
||
|
||
**Do not correct for that blindly.** Once the negotiated length reaches
|
||
`upstream_header`, the upstream form and a conforming single-clock form produce
|
||
datagrams of the same size, and nothing distinguishes them — the second 8-byte
|
||
region is a timestamp under one form and ordinary payload under the other. A
|
||
client MAY take its monotonic value from the second region **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`. In the equal-length
|
||
regime (`upstream_0_9_1_reply_len == compatible_reply_len`) the value SHOULD
|
||
be recorded as ambiguous rather than guessed. Either way the packet MUST still be
|
||
accepted, and a client that applies no correction MUST NOT fail on the resulting
|
||
implausible value. See the server specification, Section 11.3.1.
|
||
[**Verified 2026-08-11**, extended **2026-08-11** with a negotiated-length sweep
|
||
— see Section 19.14.]
|
||
|
||
### 8.5.1 Close Request Packet
|
||
|
||
**Direction:** Client → Server
|
||
**Flags:** Close (0x04) set. Reply (0x02) clear. HMAC (0x08) if applicable.
|
||
|
||
**Header fields present:**
|
||
- Magic (3 bytes)
|
||
- Flags (1 byte)
|
||
- HMAC (16 bytes) — if applicable
|
||
- Connection Token (8 bytes)
|
||
|
||
No sequence number, no payload.
|
||
|
||
### 8.6 Parameter Serialization Format
|
||
|
||
Parameters are serialized as a sequence of tag-value pairs in the packet
|
||
payload during the open request and open reply.
|
||
|
||
Each parameter is encoded as:
|
||
1. **Tag:** Unsigned varint (LEB128 encoding).
|
||
2. **Value:** Signed varint (zigzag + LEB128 encoding), EXCEPT for
|
||
string-typed values.
|
||
|
||
For string-typed values, the value is:
|
||
1. Length as unsigned varint.
|
||
2. Raw UTF-8 bytes.
|
||
|
||
**Varint encoding:** This uses the same variable-length integer encoding as
|
||
Google Protocol Buffers:
|
||
- **Unsigned varint (uvarint):** Standard unsigned LEB128. Each byte uses
|
||
the high bit as a continuation flag. The remaining 7 bits contribute to
|
||
the value, least significant group first.
|
||
- **Signed varint (varint):** Uses zigzag encoding: `(v << 1) ^ (v >> 63)`
|
||
for encoding, decoded as `(uv >> 1) ^ -(uv & 1)`. This maps signed
|
||
integers to unsigned integers so that small-magnitude values (positive and
|
||
negative) have compact encodings.
|
||
|
||
See Section 19.4 for cross-language verification notes.
|
||
|
||
**Parameter tags:**
|
||
|
||
| Tag | Name | Value Type | Unit / Encoding | Description |
|
||
|-----|------|-----------|-----------------|-------------|
|
||
| 1 | ProtocolVersion | Signed varint | Integer | Protocol version (currently 1) |
|
||
| 2 | Duration | Signed varint | Nanoseconds | Test duration |
|
||
| 3 | Interval | Signed varint | Nanoseconds | Send interval |
|
||
| 4 | Length | Signed varint | Bytes | Packet length |
|
||
| 5 | ReceivedStats | Signed varint | Enum (see below) | Server received packet statistics mode |
|
||
| 6 | StampAt | Signed varint | Enum (see below) | Server timestamp mode |
|
||
| 7 | Clock | Signed varint | Enum (see below) | Clock selection for timestamps |
|
||
| 8 | DSCP | Signed varint | Integer (TOS byte value) | IP TOS/Traffic Class byte value (see Section 10.8) |
|
||
| 9 | ServerFill | String (length-prefixed) | UTF-8, max 32 bytes | Server payload fill mode |
|
||
|
||
Parameters with a zero value MAY be omitted from serialization (they will
|
||
default to zero on the receiving end).
|
||
|
||
Unknown parameter tags MUST be silently ignored by the receiver.
|
||
|
||
There is **no protocol limit** on the size of a received parameter payload; a
|
||
receiver MUST tolerate a payload larger than any it would itself produce.
|
||
Allocating at least 128 bytes for the parameter buffer is a safe local
|
||
implementation choice, not a protocol rule (see Section 19.10).
|
||
|
||
**ReceivedStats enum values:**
|
||
|
||
| Value | Name | Meaning |
|
||
|-------|------|---------|
|
||
| 0 | None | No received packet statistics |
|
||
| 1 | Count | Include received packet count |
|
||
| 2 | Window | Include 64-bit received window |
|
||
| 3 | Both | Include both count and window |
|
||
|
||
**StampAt enum values:**
|
||
|
||
| Value | Name | Meaning |
|
||
|-------|------|---------|
|
||
| 0 | None | No server timestamps |
|
||
| 1 | Send | Timestamp at server send |
|
||
| 2 | Receive | Timestamp at server receive |
|
||
| 3 | Both | Timestamps at both server receive and send |
|
||
| 4 | Midpoint | Midpoint timestamp (average of receive and send) |
|
||
|
||
**Clock enum values:**
|
||
|
||
| Value | Name | Meaning |
|
||
|-------|------|---------|
|
||
| 1 | Wall | Wall clock only |
|
||
| 2 | Monotonic | Monotonic clock only |
|
||
| 3 | Both | Both wall and monotonic clocks |
|
||
|
||
---
|
||
|
||
## 9. Versioning and Compatibility
|
||
|
||
### 9.1 Protocol Version
|
||
|
||
The protocol version is negotiated during the open phase. The current (and
|
||
only) protocol version is **1**.
|
||
|
||
A conforming client MUST set `ProtocolVersion` to 1 in its open request.
|
||
|
||
A conforming client MUST verify that the server's returned protocol version
|
||
matches exactly. If there is a mismatch, the client MUST abort the session.
|
||
|
||
**Do not depend on the server to signal the mismatch.** Upstream 0.9.1 servers
|
||
accept any value in the `ProtocolVersion` parameter — including 0, 2, a negative
|
||
value, or the tag being absent entirely — rewrite it to 1, create a normal
|
||
session, and return an open reply with the Close flag clear and a non-zero
|
||
token. A server MAY reject an unknown version by replying with the Close flag
|
||
set and a zero token, but existing servers do not.
|
||
[**Verified 2026-08-11** — see Section 19.13.]
|
||
|
||
### 9.2 Unknown Fields and Extensions
|
||
|
||
Unknown parameter tags in the negotiation payload MUST be silently ignored.
|
||
|
||
Reserved flag bits (4-7) MUST NOT be set. Packets with unknown flag bits set
|
||
MUST be rejected.
|
||
|
||
### 9.3 Server Versions
|
||
|
||
This specification targets interoperability with servers implementing
|
||
protocol version 1, as found in IRTT versions 0.9.0 and later. Earlier
|
||
development versions (0.1.x) used an incompatible protocol and are not
|
||
supported.
|
||
|
||
---
|
||
|
||
## 10. Client Configuration Parameters
|
||
|
||
The following configuration parameters affect protocol behavior:
|
||
|
||
### 10.1 Server Address
|
||
|
||
- **Effect:** Determines the destination for all packets.
|
||
- **Default port:** 2112.
|
||
- **Valid values:** Hostname, IPv4 address, IPv6 address (in brackets),
|
||
optionally with port.
|
||
|
||
### 10.2 Duration
|
||
|
||
- **Effect:** How long the client sends test packets.
|
||
- **Default:** 60 seconds (1 minute).
|
||
- **Valid values:** Positive duration.
|
||
- **Protocol:** Sent as nanoseconds in the Duration parameter. The server
|
||
MAY reduce this value.
|
||
|
||
### 10.3 Interval
|
||
|
||
- **Effect:** Time between successive test packets.
|
||
- **Default:** 1 second.
|
||
- **Valid values:** Positive duration.
|
||
- **Protocol:** Sent as nanoseconds in the Interval parameter. The server
|
||
MAY increase this value (enforce minimum interval). The server MAY also
|
||
decrease it to stay within its timeout constraints.
|
||
|
||
### 10.4 Length
|
||
|
||
- **Effect:** Total UDP payload length of each test packet.
|
||
- **Default:** 0, which means the packet is the minimum size needed for the
|
||
header fields.
|
||
- **Valid values:** 0 or positive integer. The actual minimum is determined
|
||
by the header fields present.
|
||
- **Protocol:** Sent in the Length parameter. The server MAY reduce this.
|
||
|
||
### 10.5 Received Stats
|
||
|
||
- **Effect:** What statistics the server includes about received packets.
|
||
- **Default:** Both (count + window).
|
||
- **Valid values:** None (0), Count (1), Window (2), Both (3).
|
||
|
||
### 10.6 Timestamp Mode (StampAt)
|
||
|
||
- **Effect:** When the server records timestamps.
|
||
- **Default:** Both (send + receive).
|
||
- **Valid values:** None (0), Send (1), Receive (2), Both (3), Midpoint (4).
|
||
- **Note:** The server MAY restrict timestamps (e.g., allow only single or
|
||
no timestamps).
|
||
|
||
### 10.7 Clock Selection
|
||
|
||
- **Effect:** Which clock(s) are used for server timestamps.
|
||
- **Default:** Both (wall + monotonic).
|
||
- **Valid values:** Wall (1), Monotonic (2), Both (3).
|
||
|
||
### 10.8 DSCP
|
||
|
||
- **Effect:** Sets the TOS/Traffic Class byte in the IP header for both
|
||
client and server packets during the active test phase.
|
||
- **Default:** 0 (best effort).
|
||
- **Wire encoding (protocol fact):** the DSCP parameter carries the **raw IP TOS
|
||
/ Traffic Class byte**, range **0–255** — not a 6-bit codepoint. The
|
||
differentiated-services codepoint occupies the upper 6 bits of that byte and
|
||
the lower 2 (ECN) are typically zero, so Expedited Forwarding is **0xb8
|
||
(184)** on the wire. Other common values: 0 (best effort), 0xa0 (CS5), 0x20
|
||
(CS1). [**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md
|
||
Finding B.]
|
||
- **Out of scope — user-facing notation.** Whether an implementation's command
|
||
line and configuration accept a 0–63 codepoint (shifting it left by two to
|
||
produce the wire byte) or the raw 0–255 byte is a presentation decision for
|
||
that project, to be made and implemented separately. It is not a protocol
|
||
fact and this specification does not prescribe it.
|
||
- **Protocol:** The server echoes packets with the negotiated DSCP value.
|
||
The server MAY disallow DSCP and reset it to 0.
|
||
- **Observed behavior:** DSCP/TOS is applied only to echo request and echo
|
||
reply packets. Open and close packets use TOS=0 regardless of the
|
||
negotiated DSCP value. [**Verified 2026-04-28** — Finding C.]
|
||
|
||
### 10.9 Open Timeouts
|
||
|
||
- **Effect:** Controls retransmission during the open phase.
|
||
- **Default:** 1s, 2s, 4s, 8s.
|
||
- **Valid values:** List of positive durations, each >= 200ms.
|
||
|
||
### 10.10 Wait (Drain Period)
|
||
|
||
- **Effect:** How long to wait after the last send for outstanding replies.
|
||
- **Default:** 3 times the maximum observed RTT, falling back to 4 seconds
|
||
if no RTT has been measured.
|
||
- **Variants:**
|
||
- A factor of maximum observed RTT, with a fallback duration if no RTT
|
||
is available.
|
||
- A factor of mean observed RTT, with a fallback duration if no RTT is
|
||
available.
|
||
- A fixed duration.
|
||
|
||
### 10.11 HMAC Key
|
||
|
||
- **Effect:** When set, an MD5-based HMAC is computed over each packet and
|
||
included in the HMAC field. The server MUST also have the same key.
|
||
- **Default:** None (no HMAC).
|
||
- **Protocol:** When an HMAC key is configured, the HMAC flag (0x08) is set
|
||
in every packet.
|
||
|
||
### 10.12 Server Fill
|
||
|
||
- **Effect:** Requests the server to fill reply payloads with specific data.
|
||
- **Default:** Not specified (server uses its default fill).
|
||
- **Valid values:** String, max 32 characters. Common values: "none", "rand",
|
||
"pattern:XXXX" (hex pattern).
|
||
- **Protocol:** The server MAY restrict this to allowed fill types.
|
||
|
||
### 10.13 No-Test Mode
|
||
|
||
- **Effect:** Opens a connection and immediately closes it without running a
|
||
test. Useful for parameter validation.
|
||
- **Protocol:** Both the Open and Close flags are set in the open request.
|
||
|
||
### 10.14 Loose Mode
|
||
|
||
- **Effect:** When enabled, the client accepts server-restricted parameters
|
||
with a warning instead of aborting.
|
||
- **Default:** Disabled (strict mode).
|
||
|
||
---
|
||
|
||
## 11. Timing Semantics
|
||
|
||
### 11.1 Timestamp Capture Points
|
||
|
||
- **Client send timestamp:** SHOULD be captured as close as possible to the
|
||
send system call. For best RTT accuracy, the timestamp should be taken
|
||
immediately after the send call returns.
|
||
- **Client receive timestamp:** SHOULD be captured as close as possible to
|
||
the receive system call return, before any packet processing.
|
||
- **Server timestamps:** Determined by the negotiated StampAt parameter:
|
||
- Receive: the server's arrival instant for the request.
|
||
- Send: the server's departure instant for the reply.
|
||
- Midpoint: the average of the two.
|
||
|
||
A client can rely on the pair bracketing the server's handling of that request,
|
||
and on receive never being later than send. It cannot observe, and MUST NOT
|
||
assume, anything about where a given server takes those readings internally or
|
||
how much of its own send and receive path falls outside them.
|
||
|
||
### 11.2 Clock Types
|
||
|
||
A conforming client MUST track both wall clock and monotonic clock times
|
||
for client-side timestamps. The monotonic clock SHOULD be used for all
|
||
duration calculations (RTT, IPDV) because it is immune to system time
|
||
adjustments. The wall clock is needed for one-way delay calculations.
|
||
|
||
### 11.3 Send Scheduling
|
||
|
||
The client sends packets on a **fixed isochronous schedule**:
|
||
|
||
1. Record the start time.
|
||
2. The ideal send time for packet N is: `start + N * interval`.
|
||
3. The client SHOULD send each packet as close to its ideal send time as
|
||
the system timer allows.
|
||
4. The client SHOULD compensate for cumulative timer drift so that packets
|
||
remain aligned to integer multiples of the interval from the start time.
|
||
The specific drift-compensation strategy is implementation-defined.
|
||
5. Before each send, the client checks whether the next scheduled time
|
||
would be at or past `start + duration`. If so, sending stops.
|
||
|
||
The observable effect is that packets are sent at approximately
|
||
`start + 0`, `start + interval`, `start + 2*interval`, etc., with the
|
||
last packet sent strictly before `start + duration`.
|
||
|
||
### 11.4 Duration and Count Interaction
|
||
|
||
- The expected packet count is `ceil(duration / interval)`.
|
||
[**Verified 2026-04-28** — see BLACKBOX_VERIFICATION_REPORT.md Finding A.]
|
||
- The duration is **exclusive**: the last packet is sent at approximately
|
||
`start + (ceil(duration / interval) - 1) * interval`, which is strictly
|
||
before `start + duration`.
|
||
- Example: duration=1s, interval=200ms → packets at 0ms, 200ms, 400ms,
|
||
600ms, 800ms = 5 packets = ceil(1000/200).
|
||
- Example: duration=1s, interval=1s → packet at 0ms only = 1 packet =
|
||
ceil(1000/1000). The packet at 1000ms is NOT sent (exclusive end).
|
||
|
||
### 11.5 Timer Compensation
|
||
|
||
The client SHOULD use timer error compensation to improve send accuracy.
|
||
The specific compensation algorithm is implementation-defined. Any approach
|
||
that produces accurate isochronous sending is acceptable.
|
||
|
||
### 11.6 Drain Period
|
||
|
||
After the last packet is sent, if not all replies have been received, the
|
||
client MUST wait for outstanding replies for the configured wait duration
|
||
before closing the session.
|
||
|
||
---
|
||
|
||
## 12. Measurement Semantics
|
||
|
||
### 12.1 Round-Trip Time (RTT)
|
||
|
||
RTT MUST be calculated using client-side monotonic clock values:
|
||
|
||
```
|
||
RTT = client_receive_mono - client_send_mono
|
||
```
|
||
|
||
If both server send and receive timestamps are available (monotonic
|
||
preferred, wall clock as fallback), the server processing time SHOULD be
|
||
subtracted:
|
||
|
||
```
|
||
server_processing_time = server_send - server_receive
|
||
RTT = (client_receive_mono - client_send_mono) - server_processing_time
|
||
```
|
||
|
||
This subtraction SHOULD only be applied when both server timestamps are
|
||
available. See Section 19.12 for edge-case behavior when server processing
|
||
time appears to exceed the raw round-trip time.
|
||
|
||
### 12.2 One-Way Delay
|
||
|
||
One-way delay requires wall clock timestamps and externally synchronized
|
||
clocks (e.g., NTP, PTP). Values are only meaningful when clocks are
|
||
synchronized.
|
||
|
||
```
|
||
send_delay = server_best_receive_wall - client_send_wall
|
||
receive_delay = client_receive_wall - server_best_send_wall
|
||
```
|
||
|
||
Where "best receive" prefers the actual receive timestamp, falling back to
|
||
the send timestamp if receive is not available (and vice versa for "best
|
||
send"). For midpoint timestamps, both best_receive and best_send are the
|
||
midpoint value.
|
||
|
||
### 12.3 Packet Loss
|
||
|
||
```
|
||
packet_loss_percent = 100 * (packets_sent - unique_replies_received) / packets_sent
|
||
```
|
||
|
||
If the received count is available from the server:
|
||
|
||
```
|
||
upstream_loss_percent = 100 * (packets_sent - server_packets_received) / packets_sent
|
||
downstream_loss_percent = 100 * (server_packets_received - unique_replies_received) / server_packets_received
|
||
```
|
||
|
||
### 12.4 Duplicate Packets
|
||
|
||
A duplicate is a reply with a sequence number for which a reply has already
|
||
been recorded. Duplicates MUST be counted but MUST NOT update the original
|
||
round-trip data for that sequence number.
|
||
|
||
```
|
||
duplicate_percent = 100 * duplicates / total_packets_received
|
||
```
|
||
|
||
### 12.5 Late Packets (Out-of-Order)
|
||
|
||
A late packet is one whose sequence number is lower than the most recently
|
||
received sequence number.
|
||
|
||
```
|
||
late_packets_percent = 100 * late_packets / total_packets_received
|
||
```
|
||
|
||
### 12.6 IPDV (Jitter)
|
||
|
||
IPDV is the difference in delay between two successive successfully received
|
||
replies. It is only calculable for sequence number N when both packet N and
|
||
packet N-1 have received replies.
|
||
|
||
```
|
||
round_trip_ipdv[N] = RTT[N] - RTT[N-1]
|
||
send_ipdv[N] = send_delay[N] - send_delay[N-1] (if timestamps available)
|
||
receive_ipdv[N] = receive_delay[N] - receive_delay[N-1] (if timestamps available)
|
||
```
|
||
|
||
For send and receive IPDV, monotonic clock differences are RECOMMENDED
|
||
over wall clock differences when both are available, as the monotonic clock
|
||
is not subject to NTP adjustments. Note that IPDV does NOT require
|
||
synchronized clocks — only consistent clocks within each endpoint.
|
||
|
||
### 12.7 Server Processing Time
|
||
|
||
When both server send and receive timestamps are available:
|
||
|
||
```
|
||
server_processing_time = server_send - server_receive
|
||
```
|
||
|
||
Prefer monotonic clock values if available; fall back to wall clock.
|
||
|
||
### 12.8 Per-Packet Upstream/Downstream Loss (Received Window)
|
||
|
||
When the 64-bit received window is present in a reply:
|
||
- Bit 0 (LSB) represents the current packet (the one being replied to).
|
||
- Bit 1 represents the packet with sequence number one less than current, etc.
|
||
- A set bit means the server received that packet; a clear bit means it
|
||
did not.
|
||
|
||
Bit 0 (LSB) is always set in a window emitted by a server, since it represents
|
||
the current packet which was received. [**Verified 2026-04-28** — see
|
||
Section 19.7.] Implementations SHOULD treat a window value of 0 as potentially
|
||
invalid and avoid using it for loss classification. Note that upstream 0.9.1
|
||
servers never emit 0 when the window is negotiated at all.
|
||
[**Verified 2026-08-11** — see Section 19.15.]
|
||
|
||
**A window of `0x1` carries no historical information.** Upstream 0.9.1 servers
|
||
reset the window to `0x1` — not merely fail to update it — whenever a request
|
||
arrives with a sequence number lower than the highest already seen, or with a
|
||
gap of 64 or more from the previous request. `0x1` does **not** mean the previous
|
||
63 packets were lost; it means no useful prior received-history is represented in
|
||
the window after that transition. A single reordered packet discards everything
|
||
the window knew about the previous 63 sequence numbers, and later packets rebuild
|
||
it from scratch. A client that reads `0x1` literally will misclassify up to 63
|
||
earlier packets as lost upstream after one reordering event.
|
||
|
||
A conforming client SHOULD treat a window value of `0x1` on any reply other than
|
||
the first of the session as "no historical information available" rather than as
|
||
"the server received nothing earlier."
|
||
[**Verified 2026-08-11** — see Section 19.15 and
|
||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1.]
|
||
|
||
The client processes the received window to classify previous packets:
|
||
- If a packet was not received by the client but the window says the server
|
||
received it → lost downstream.
|
||
- If a packet was not received by the client and the window says the server
|
||
did not receive it → lost upstream.
|
||
- If the window cannot confirm (e.g., more than 64 packets ago, or the window
|
||
carries no history) → generic loss.
|
||
|
||
The window has a maximum lookback of 63 packets (bits 1-63).
|
||
|
||
**Scope note.** The paragraphs above state what the window field *represents* on
|
||
the wire, which is what was verified. The downstream/upstream loss
|
||
classification built on top of it is a client-side interpretation question that
|
||
has not been audited to the same standard here, and it is scheduled for its own
|
||
review on the implementation side. Treat the classification rules as a starting
|
||
point rather than as verified protocol semantics.
|
||
|
||
### 12.9 Statistics
|
||
|
||
For RTT, send delay, receive delay, and IPDV, a conforming client SHOULD
|
||
calculate:
|
||
- Minimum, Maximum, Mean, Median, Standard Deviation, Variance.
|
||
- Total count (N).
|
||
|
||
Median requires storing all values. The method of computing running
|
||
statistics is implementation-defined.
|
||
|
||
---
|
||
|
||
## 13. Validation Rules
|
||
|
||
### 13.1 Magic Bytes
|
||
|
||
Every received packet MUST begin with the magic bytes `0x14 0xA7 0x5B`.
|
||
Packets with incorrect magic MUST be discarded.
|
||
|
||
### 13.2 Flag Validation
|
||
|
||
Reserved flag bits (4-7) MUST be zero. Packets with invalid flag bits MUST
|
||
be discarded.
|
||
|
||
### 13.3 Reply Flag
|
||
|
||
All packets received by the client MUST have the Reply flag (0x02) set.
|
||
Packets without the Reply flag MUST be discarded.
|
||
|
||
### 13.4 HMAC Validation
|
||
|
||
If an HMAC key is configured:
|
||
- Received packets MUST have the HMAC flag set.
|
||
- The HMAC MUST be validated:
|
||
1. Extract the HMAC value from the packet.
|
||
2. Zero the HMAC field in the packet buffer.
|
||
3. Compute HMAC-MD5 over the entire packet buffer.
|
||
4. Compare the computed HMAC with the extracted value using a
|
||
constant-time comparison.
|
||
- Packets with missing or invalid HMAC MUST be discarded.
|
||
|
||
If no HMAC key is configured:
|
||
- Received packets MUST NOT have the HMAC flag set. Packets with an
|
||
unexpected HMAC MUST be discarded.
|
||
|
||
### 13.5 Sequence Number Validation
|
||
|
||
Echo reply sequence numbers MUST correspond to a previously sent packet.
|
||
Replies with sequence numbers outside the range [0, packets_sent) MUST be
|
||
rejected.
|
||
|
||
### 13.6 Timestamp Consistency
|
||
|
||
If both receive and send timestamps are present in a reply, they MUST use
|
||
the same clock mode (both wall, both monotonic, or both sets). Inconsistent
|
||
clock modes MUST cause the packet to be rejected.
|
||
|
||
Midpoint timestamps MUST be exclusive — if a midpoint timestamp is present,
|
||
no receive or send timestamp fields may also be present.
|
||
|
||
### 13.7 Close Flag in Echo Reply
|
||
|
||
A server MAY forcibly end a session mid-test by setting the Close flag (0x04) on
|
||
an otherwise complete echo reply. This is the only in-band mechanism for a
|
||
server-initiated close; there is no standalone server close packet.
|
||
|
||
A client MUST handle this case. On receiving an echo reply with the Close flag
|
||
set, the client MUST stop the test, MUST NOT send further test packets for that
|
||
session, and SHOULD report the partial results gathered so far as a successful
|
||
(if truncated) run rather than as an error.
|
||
|
||
Whether the close-flagged reply's own measurement is recorded is an
|
||
implementation choice. Upstream 0.9.1 clients **discard** it: the run is
|
||
terminated on the "server closed" condition before the measurement is recorded.
|
||
|
||
The session token is invalid from that moment; a close request the client sends
|
||
afterwards will be silently dropped by the server, which is normal.
|
||
|
||
[**Verified 2026-08-11** — see Section 19.3 and
|
||
`IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2.]
|
||
|
||
---
|
||
|
||
## 14. Error Handling
|
||
|
||
| Error Category | Client Behavior |
|
||
|---------------|----------------|
|
||
| Name resolution failure | MUST abort. |
|
||
| Socket creation / bind failure | MUST abort. |
|
||
| Server unreachable (all open timeouts exhausted) | MUST abort. No partial results. |
|
||
| Protocol version mismatch | MUST abort. |
|
||
| Server restriction rejected (strict mode) | MUST abort. |
|
||
| Server restriction accepted (loose mode) | SHOULD warn, continue. |
|
||
| DSCP not supported by OS | MUST abort if DSCP was requested non-zero. |
|
||
| DF bit not supported by OS | MUST abort if DF was requested non-default. |
|
||
| Open reply with Close flag (not no-test) | MUST abort (server rejected session). |
|
||
| Zero connection token (without close flag) | MUST abort. |
|
||
| Send error during test | SHOULD abort, report partial results. |
|
||
| Receive error during test | SHOULD abort, report partial results. |
|
||
| Individual malformed packet during test | SHOULD abort. |
|
||
| Short reply (shorter than `compatible_reply_len`, the normal compatible reply length — not merely shorter than the negotiated Length) | SHOULD abort. |
|
||
| Reply at an unexpected length (any length other than `compatible_reply_len`, or the single-clock midpoint form at exactly `upstream_0_9_1_reply_len`) | SHOULD abort. See Section 8.5. |
|
||
| Stamp-at mismatch in reply | SHOULD abort. |
|
||
| Clock mismatch in reply | SHOULD abort. |
|
||
| Unexpected sequence number | SHOULD abort. |
|
||
| Context cancellation (user interrupt) | SHOULD stop gracefully, close connection, report partial results. |
|
||
| Insufficient resources for test parameters | MUST abort. |
|
||
|
||
---
|
||
|
||
## 15. Result Model
|
||
|
||
After a test, the client implementation SHOULD expose the following
|
||
information:
|
||
|
||
### 15.1 Per-Packet Data (Round Trips)
|
||
|
||
For each sent packet (indexed by sequence number):
|
||
- Sequence number.
|
||
- Lost status: not lost, lost (generic), lost upstream, lost downstream.
|
||
- Client send timestamp (wall + monotonic).
|
||
- Client receive timestamp (wall + monotonic) — empty if lost.
|
||
- Server receive timestamp (wall and/or monotonic) — as negotiated.
|
||
- Server send timestamp (wall and/or monotonic) — as negotiated.
|
||
- Computed RTT.
|
||
- Computed send delay.
|
||
- Computed receive delay.
|
||
- Computed IPDV (round-trip, send, receive) relative to previous packet.
|
||
|
||
### 15.2 Aggregate Statistics
|
||
|
||
- Test start time (wall + monotonic).
|
||
- Actual test duration.
|
||
- Packets sent.
|
||
- Packets received (unique).
|
||
- Packet loss percentage.
|
||
- Upstream loss percentage (if server count available).
|
||
- Downstream loss percentage (if server count available).
|
||
- Duplicate count and percentage.
|
||
- Late packet count and percentage.
|
||
- RTT statistics: min, max, mean, median, stddev, variance.
|
||
- Send delay statistics (if timestamps available).
|
||
- Receive delay statistics (if timestamps available).
|
||
- IPDV statistics (round-trip, send, receive).
|
||
- Server processing time statistics.
|
||
- Bytes sent and received.
|
||
- Send and receive bitrate.
|
||
- Timer error statistics.
|
||
- Timer miss count and percentage.
|
||
- Wait duration actually used.
|
||
|
||
### 15.3 Configuration
|
||
|
||
- Local and remote addresses (resolved).
|
||
- Negotiated parameters.
|
||
- Originally supplied parameters.
|
||
- IP version used.
|
||
|
||
### 15.4 Errors
|
||
|
||
- Send error (if the send operation failed).
|
||
- Receive error (if the receive operation failed).
|
||
|
||
---
|
||
|
||
## 16. Interoperability Requirements
|
||
|
||
1. A conforming client MUST be able to open a session with a compatible IRTT
|
||
server running protocol version 1.
|
||
|
||
2. A conforming client MUST send open requests with correctly serialized
|
||
parameters using the varint-encoded tag-value format.
|
||
|
||
3. A conforming client MUST correctly parse server open replies, including
|
||
restricted parameters.
|
||
|
||
4. A conforming client MUST include the connection token in all post-open
|
||
packets.
|
||
|
||
5. A conforming client MUST send test packets in the negotiated format, with
|
||
the correct field layout and packet length.
|
||
|
||
6. A conforming client MUST correctly match replies to sent packets using
|
||
sequence numbers.
|
||
|
||
7. A conforming client MUST tolerate UDP reordering, duplication, and loss.
|
||
|
||
8. A conforming client MUST validate magic bytes, flags, and HMAC on all
|
||
received packets.
|
||
|
||
9. A conforming client SHOULD produce RTT measurements consistent with
|
||
upstream implementations within the precision of the system clocks.
|
||
|
||
10. A conforming client MUST send a close packet at the end of the session.
|
||
|
||
11. A conforming client MUST correctly handle the HMAC calculation:
|
||
compute HMAC-MD5 over the full packet with the HMAC field zeroed.
|
||
|
||
12. When computing an outgoing HMAC, the client MUST:
|
||
a. Set the HMAC flag bit.
|
||
b. Zero the HMAC field.
|
||
c. Compute HMAC-MD5 over the entire packet buffer.
|
||
d. Write the resulting MAC into the HMAC field.
|
||
|
||
---
|
||
|
||
## 17. Black-Box Test Plan
|
||
|
||
### Test 1: Basic Connectivity
|
||
|
||
- **Setup:** Start an upstream IRTT server with default settings.
|
||
- **Client config:** Default parameters (1s interval, 1m duration), reduce
|
||
to 5s duration for testing. Server address: localhost.
|
||
- **Expected:** Session opens, packets are exchanged, results include RTT
|
||
measurements, session closes cleanly.
|
||
- **Pass criteria:** Non-zero packets_sent and packets_received. RTT values
|
||
present. No errors.
|
||
|
||
### Test 2: Parameter Negotiation
|
||
|
||
- **Setup:** Start a server with `--max-length=100`.
|
||
- **Client config:** Request length 200.
|
||
- **Expected (strict mode):** Client aborts with server restriction error.
|
||
- **Expected (loose mode):** Client continues with length 100.
|
||
- **Pass criteria (strict):** Client exits with error indicating server
|
||
reduced length.
|
||
- **Pass criteria (loose):** Successful test with negotiated length 100.
|
||
|
||
### Test 3: HMAC Authentication
|
||
|
||
- **Setup:** Start server with `--hmac=testkey`.
|
||
- **Client config:** `--hmac=testkey`.
|
||
- **Expected:** Session succeeds.
|
||
- **Pass criteria:** Successful test with no errors.
|
||
- **Variant:** Client uses wrong key. Expected: open timeout (server drops
|
||
packets with bad HMAC silently).
|
||
|
||
### Test 4: Packet Capture Verification
|
||
|
||
- **Setup:** Start server on localhost. Capture with tcpdump on the loopback
|
||
interface, port 2112.
|
||
- **Client config:** Short test (5s, 1s interval).
|
||
- **Expected:** Capture shows:
|
||
- Open request: magic bytes 14 a7 5b, flags=0x01, followed by varint
|
||
params in payload.
|
||
- Open reply: magic bytes, flags=0x03 (open+reply), 8-byte token, params.
|
||
- Echo requests: magic bytes, flags=0x00, 8-byte token, 4-byte sequence number.
|
||
- Echo replies: magic bytes, flags=0x02 (reply), 8-byte token, 4-byte
|
||
sequence number, optional stats and timestamps.
|
||
- Close request: magic bytes, flags=0x04.
|
||
- **Pass criteria:** All packets have correct magic, flags, and field layout.
|
||
|
||
### Test 5: Sequence Number Handling
|
||
|
||
- **Setup:** Server with default settings.
|
||
- **Client config:** 10s duration, 200ms interval (expect ~51 packets).
|
||
- **Expected:** Reply sequence numbers are in range [0, packets_sent).
|
||
Results show correct per-packet data.
|
||
- **Pass criteria:** All received replies have valid sequence numbers. No
|
||
unexpected sequence number errors.
|
||
|
||
### Test 6: Timestamp Modes
|
||
|
||
- **Setup:** Server with default settings.
|
||
- **Variants:**
|
||
- `--tstamp=none --clock=wall`
|
||
- `--tstamp=send --clock=wall`
|
||
- `--tstamp=receive --clock=monotonic`
|
||
- `--tstamp=both --clock=both`
|
||
- `--tstamp=midpoint --clock=both`
|
||
- **Expected:** Each variant produces the correct timestamp fields in replies.
|
||
RTT is always available. One-way delay is only available with wall clock
|
||
timestamps.
|
||
- **Pass criteria:** Results match expected availability of delay measurements.
|
||
|
||
### Test 7: Loss Detection
|
||
|
||
- **Setup:** Use a network impairment tool (e.g., `tc netem loss 10%`) or a
|
||
server on a lossy link.
|
||
- **Client config:** 30s duration, 100ms interval.
|
||
- **Expected:** packet_loss_percent is non-zero. With `--stats=both`,
|
||
upstream and downstream loss are differentiated.
|
||
- **Pass criteria:** Loss percentages are reported. With received window,
|
||
per-packet loss direction is classified.
|
||
|
||
### Test 8: IPv6
|
||
|
||
- **Setup:** Server listening on IPv6 (::1).
|
||
- **Client config:** `-6 ::1`.
|
||
- **Expected:** Successful test over IPv6.
|
||
- **Pass criteria:** IP version in results is IPv6.
|
||
|
||
### Test 9: No-Test Mode
|
||
|
||
- **Setup:** Server with default settings.
|
||
- **Client config:** `-n localhost`.
|
||
- **Expected:** Connection opens, parameters are negotiated, connection
|
||
closes immediately. No test packets sent.
|
||
- **Pass criteria:** No round-trip data. No errors.
|
||
|
||
### Test 10: Large Packet Size
|
||
|
||
- **Setup:** Server with default settings.
|
||
- **Client config:** `-l 1472 -d 5s localhost`.
|
||
- **Expected:** Successful test with 1472-byte packets.
|
||
- **Pass criteria:** Non-zero results.
|
||
|
||
### Test 11: Server Fill
|
||
|
||
- **Setup:** Server with `--allow-fills=rand`.
|
||
- **Client config:** `--sfill=rand -l 172 -d 5s localhost`.
|
||
- **Expected:** Successful test. Server fills reply payloads with random data.
|
||
- **Pass criteria:** Test completes without error.
|
||
|
||
### Test 12: DSCP
|
||
|
||
- **Setup:** Server with default settings (DSCP allowed).
|
||
- **Client config:** `--dscp=46 -d 5s localhost`.
|
||
- **Expected:** Packets are sent with EF DSCP marking. Capture verification
|
||
shows TOS/TC field set.
|
||
- **Pass criteria:** Test completes. Packet capture confirms DSCP value.
|
||
|
||
---
|
||
|
||
## 18. Packet Test Vectors
|
||
|
||
### 18.1 Echo Request with HMAC
|
||
|
||
**Scenario:** Echo request with HMAC, received stats (both), timestamps
|
||
(both, both clocks), pattern fill `0xFF 0xFE 0xFD 0xFC`, 92-byte packet
|
||
(76-byte header + 16-byte payload).
|
||
[**Size corrected 2026-04-28** — originally described as 256 bytes, but
|
||
HMAC verification confirms the test vector is 92 bytes. See
|
||
BLACKBOX_VERIFICATION_REPORT.md Finding E.]
|
||
|
||
**Connection token:** `0x886bc9a722b33eea` (little-endian)
|
||
**Sequence number:** `0x6fe2a1bb` (little-endian)
|
||
**HMAC key:** `0x3c 0x68 0x1d 0x39 0x41 0x1d 0x72 0x43`
|
||
|
||
**Fields present (in order):**
|
||
1. Magic: 3 bytes
|
||
2. Flags: 1 byte (0x08 = HMAC)
|
||
3. HMAC: 16 bytes
|
||
4. Connection Token: 8 bytes
|
||
5. Sequence Number: 4 bytes
|
||
6. Received Count: 4 bytes (zeroed)
|
||
7. Received Window: 8 bytes (zeroed)
|
||
8. Receive Wall: 8 bytes (zeroed)
|
||
9. Receive Mono: 8 bytes (zeroed)
|
||
10. Send Wall: 8 bytes (zeroed)
|
||
11. Send Mono: 8 bytes (zeroed)
|
||
|
||
**Header total:** 3+1+16+8+4+4+8+8+8+8+8 = 76 bytes
|
||
**Payload:** 92 - 76 = 16 bytes of repeating pattern
|
||
|
||
**Hex (92 bytes):**
|
||
```
|
||
14 a7 5b 08 e7 03 41 e7 d4 08 cf 69 41 f3 f4 78
|
||
5a 56 0c 4c ea 3e b3 22 a7 c9 6b 88 bb a1 e2 6f
|
||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
||
00 00 00 00 00 00 00 00 00 00 00 00 ff fe fd fc
|
||
ff fe fd fc ff fe fd fc ff fe fd fc
|
||
```
|
||
|
||
**Field breakdown:**
|
||
- Bytes 0-2: Magic `14 a7 5b`
|
||
- Byte 3: Flags `08` (HMAC set)
|
||
- Bytes 4-19: HMAC `e7 03 41 e7 d4 08 cf 69 41 f3 f4 78 5a 56 0c 4c`
|
||
- Bytes 20-27: Connection Token `ea 3e b3 22 a7 c9 6b 88` (LE: 0x886bc9a722b33eea)
|
||
- Bytes 28-31: Sequence Number `bb a1 e2 6f` (LE: 0x6fe2a1bb)
|
||
- Bytes 32-35: Received Count (zeroed)
|
||
- Bytes 36-43: Received Window (zeroed)
|
||
- Bytes 44-51: Receive Wall Timestamp (zeroed)
|
||
- Bytes 52-59: Receive Mono Timestamp (zeroed)
|
||
- Bytes 60-67: Send Wall Timestamp (zeroed)
|
||
- Bytes 68-75: Send Mono Timestamp (zeroed)
|
||
- Bytes 76+: Payload (pattern fill)
|
||
|
||
**Client behavior:** This packet is sent to the server during the active test phase.
|
||
|
||
### 18.2 Echo Reply with HMAC and Full Timestamps
|
||
|
||
**Scenario:** Echo reply with HMAC, received stats (both), timestamps (both,
|
||
both clocks), pattern fill, 92-byte packet (76-byte header + 16-byte payload).
|
||
[**Size corrected 2026-04-28** — see Finding E.]
|
||
|
||
**Connection token:** `0xe666ceb6766fcbc3` (little-endian)
|
||
**Sequence number:** `0x1d3b0706` (little-endian)
|
||
**HMAC key:** `0xda 0xb3 0xe9 0x04 0xa6 0x87 0x92 0x49`
|
||
**Received count:** `0xa3bc9f19` (little-endian)
|
||
**Received window:** `0xd7e939e586f83b9b` (little-endian)
|
||
|
||
**Timestamps (little-endian int64):**
|
||
- Receive wall: `0x525af13dee2a75a1`
|
||
- Receive mono: `0x2d5562223e4ac69a`
|
||
- Send wall: `0x589705d446293f69`
|
||
- Send mono: `0x461df12fdd2c5066`
|
||
|
||
**Fields present (in order):**
|
||
1. Magic: 3 bytes
|
||
2. Flags: 1 byte (0x08 = HMAC, plus 0x02 = Reply → but let's verify from the test vector)
|
||
3. HMAC: 16 bytes
|
||
4. Connection Token: 8 bytes
|
||
5. Sequence Number: 4 bytes
|
||
6. Received Count: 4 bytes
|
||
7. Received Window: 8 bytes
|
||
8. Receive Wall: 8 bytes
|
||
9. Receive Mono: 8 bytes
|
||
10. Send Wall: 8 bytes
|
||
11. Send Mono: 8 bytes
|
||
|
||
**Note on flags:** The flags byte in this test vector is `0x08` (only HMAC
|
||
set). This vector is provided for verifying field layout and HMAC
|
||
computation only. **In actual protocol operation, the Reply flag (0x02)
|
||
MUST also be set on all server-to-client packets**, making the expected
|
||
flags byte `0x0A` for a normal HMAC echo reply. Implementers MUST
|
||
set the Reply flag in actual protocol exchanges; the HMAC computation
|
||
itself does not depend on the Reply flag's value.
|
||
|
||
**Hex (first 92 bytes):**
|
||
```
|
||
14 a7 5b 08 d2 98 a3 4a 6a 13 41 02 68 b2 67 a8
|
||
d6 7e 28 25 c3 cb 6f 76 b6 ce 66 e6 06 07 3b 1d
|
||
19 9f bc a3 9b 3b f8 86 e5 39 e9 d7 a1 75 2a ee
|
||
3d f1 5a 52 9a c6 4a 3e 22 62 55 2d 69 3f 29 46
|
||
d4 05 97 58 66 50 2c dd 2f f1 1d 46 ff fe fd fc
|
||
ff fe fd fc ff fe fd fc ff fe fd fc
|
||
```
|
||
|
||
**Field breakdown:**
|
||
- Bytes 0-2: Magic `14 a7 5b`
|
||
- Byte 3: Flags `08` (HMAC; see note above about Reply flag)
|
||
- Bytes 4-19: HMAC `d2 98 a3 4a 6a 13 41 02 68 b2 67 a8 d6 7e 28 25`
|
||
- Bytes 20-27: Connection Token `c3 cb 6f 76 b6 ce 66 e6` (LE: 0xe666ceb6766fcbc3)
|
||
- Bytes 28-31: Sequence Number `06 07 3b 1d` (LE: 0x1d3b0706)
|
||
- Bytes 32-35: Received Count `19 9f bc a3` (LE: 0xa3bc9f19)
|
||
- Bytes 36-43: Received Window `9b 3b f8 86 e5 39 e9 d7` (LE: 0xd7e939e586f83b9b)
|
||
- Bytes 44-51: Receive Wall `a1 75 2a ee 3d f1 5a 52` (LE: 0x525af13dee2a75a1)
|
||
- Bytes 52-59: Receive Mono `9a c6 4a 3e 22 62 55 2d` (LE: 0x2d5562223e4ac69a)
|
||
- Bytes 60-67: Send Wall `69 3f 29 46 d4 05 97 58` (LE: 0x589705d446293f69)
|
||
- Bytes 68-75: Send Mono `66 50 2c dd 2f f1 1d 46` (LE: 0x461df12fdd2c5066)
|
||
- Bytes 76+: Payload (pattern fill)
|
||
|
||
**Expected client behavior:** Validate magic, validate HMAC, extract
|
||
connection token, extract sequence number, extract received stats, extract timestamps,
|
||
record round-trip data.
|
||
|
||
---
|
||
|
||
## 19. Open Questions / Verification Required
|
||
|
||
### 19.1 Open Request Field Layout — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** The open request does NOT include a connection token field. The
|
||
layout is: magic (3) + flags (1) + [HMAC (16) if applicable] + params.
|
||
Verified by packet capture (`captures/full-session.pcapng`,
|
||
`captures/hmac-session.pcapng`).
|
||
|
||
### 19.2 Minimum Packet Length Calculation — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** Minimum echo packet lengths verified for all stats/tstamp/clock
|
||
combinations. See BLACKBOX_VERIFICATION_REPORT.md Section 19.2 for the
|
||
complete table. The minimum is 16 bytes (no stats, no timestamps) and the
|
||
maximum default is 76 bytes (both+both+both+HMAC).
|
||
|
||
### 19.3 Server Close During Test — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** Yes. A server that enforces a maximum test duration answers the
|
||
first echo request arriving after its hard deadline (the configured maximum
|
||
duration plus a 2-second grace, measured from the session's first echo request)
|
||
with a complete, ordinary echo reply that additionally has the Close flag set,
|
||
and removes the session at the same moment. Measured: a 1-second maximum
|
||
produced a close-flagged reply 3.01 s after the first echo; a 2-second maximum
|
||
produced one at 4.06 s.
|
||
|
||
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 negotiation.
|
||
|
||
Observed upstream 0.9.1 client reaction: it discards the close-flagged reply's
|
||
measurement, terminates the run with a "server closed connection" condition,
|
||
prints the partial results, and exits with a success status.
|
||
|
||
No other server condition was observed to set the Close flag on an echo reply:
|
||
idle expiry, shutdown, rate limiting, oversize requests and authentication
|
||
failure were each exercised and none produced one.
|
||
|
||
See Section 13.7, `IRTT_SERVER_PROTOCOL_SPEC.md` Section 15.2, and
|
||
`captures/server-close.pcapng` (frame 36 carries flags `0x06`).
|
||
|
||
### 19.4 Varint Encoding Compatibility — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** Parameters decoded from captured packets match expected values
|
||
using standard protobuf-style zigzag + LEB128. All parameter tags and values
|
||
verified. See BLACKBOX_VERIFICATION_REPORT.md Section 19.4.
|
||
|
||
### 19.5 HMAC Computation Scope — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** HMAC-MD5 is computed over the entire packet buffer (including
|
||
payload) with the 16-byte HMAC field zeroed. Cross-checked with a separately
|
||
written Python script for all four packet types (open request, echo request, echo reply,
|
||
close request). The spec's test vectors (18.1 and 18.2) also verify
|
||
correctly at 92-byte packet size.
|
||
|
||
### 19.6 Send Timestamp Capture Timing — OPEN (not externally testable)
|
||
|
||
This is an implementation-internal detail that cannot be verified via
|
||
black-box testing. The client SHOULD capture timestamps as close to the
|
||
send/receive system calls as possible (see Section 11.1), but the exact
|
||
timing is an implementation choice.
|
||
|
||
### 19.7 Server Received Window Validity — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** Bit 0 (LSB) of the received window is always set for valid
|
||
windows (it represents the current packet which was received). Observed
|
||
window values: seq 0 → 0x01, seq 1 → 0x03, seq 2 → 0x07. A window
|
||
value of 0 can be used as the invalid sentinel. See
|
||
BLACKBOX_VERIFICATION_REPORT.md Section 19.7.
|
||
|
||
**Extended 2026-08-11.** Upstream 0.9.1 was subsequently confirmed never to emit
|
||
0 when the window is negotiated at all, and the far more common `0x1` was found
|
||
to be a *valid* window carrying no history — which is a distinct condition from
|
||
the 0 sentinel and must not be treated the same way. See Section 19.15 and
|
||
Section 12.8.
|
||
|
||
### 19.8 Open Reply Without HMAC When Client Expects HMAC — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** The server silently drops packets with missing or invalid HMAC.
|
||
The client times out with `[OpenTimeout] no reply from server`. Same
|
||
behavior for wrong key and no key. See BLACKBOX_VERIFICATION_REPORT.md
|
||
Section 19.8.
|
||
|
||
### 19.9 Minimum Restricted Interval Safety Floor — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** The safety floor is **1 second**, and it is enforced in loose mode
|
||
as well as strict mode.
|
||
|
||
The test uses an ordinary, unmodified server: a server restricts the negotiated
|
||
interval to at most one quarter of its idle timeout, so an idle timeout below
|
||
4 seconds forces a reduction below 1 second for a client requesting 1 second.
|
||
|
||
| Server idle timeout | Returned interval | Client outcome |
|
||
|---------------------|-------------------|----------------|
|
||
| 2 s | 500 ms | Aborts: reduction below 1 s — strict **and** loose |
|
||
| 3 s | 750 ms | Aborts: reduction below 1 s — strict **and** loose |
|
||
| 4 s | 1 s (no reduction for a 1 s request) | Proceeds |
|
||
|
||
A reduction to exactly 1 second (requesting 2 s against a 4 s idle timeout) is
|
||
treated as an ordinary server restriction: rejected in strict mode, accepted in
|
||
loose mode.
|
||
|
||
See `test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 4.4.
|
||
|
||
### 19.10 Maximum Parameter Buffer Size — ✅ RESOLVED (as a local choice)
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** There is no protocol limit on the size of a received parameter
|
||
payload. An open request carrying over 200 bytes of parameters (80 repetitions
|
||
of an unknown tag) was accepted and answered normally by an upstream server.
|
||
|
||
The largest payload a version-1 peer can legitimately *produce* remains small:
|
||
the observed maximum from a real client was 52 bytes, and the theoretical
|
||
maximum with all tags at their widest encodings plus a 32-byte ServerFill string
|
||
is under 90 bytes. Allocating at least 128 bytes for the parameter buffer is a
|
||
safe **implementation choice**, not a protocol requirement, and a receiver
|
||
SHOULD tolerate larger payloads rather than rejecting them.
|
||
|
||
### 19.11 Field Ordering Verification — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-04-28, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** The field ordering in Section 8.1.3 is correct. Verified with
|
||
six different field combinations including no optional fields, various
|
||
stats/tstamp combinations, midpoint timestamps, and HMAC. See
|
||
BLACKBOX_VERIFICATION_REPORT.md Section 19.11.
|
||
|
||
### 19.12 RTT When Server Processing Time Exceeds Raw Round-Trip — OPEN / Verification Required
|
||
|
||
**Status:** Not tested. Hard to trigger on localhost.
|
||
|
||
**Question:** When server processing time (as reported by server
|
||
timestamps) exceeds the client-measured round-trip time — possible due to
|
||
clock granularity or drift — the subtraction would produce a negative RTT.
|
||
Should the client skip the subtraction, clamp to zero, or report the
|
||
negative value?
|
||
|
||
This edge case requires server processing time to exceed the raw
|
||
client-measured RTT, which is extremely unlikely under normal conditions.
|
||
|
||
### 19.13 Server-Side Protocol Version Enforcement — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** Upstream servers do not enforce the protocol version. Open requests
|
||
carrying `ProtocolVersion` values of 0, 2 and −1, and requests omitting the tag
|
||
entirely, each produced a session-creating open reply with flags `0x03`
|
||
(Open|Reply), a non-zero token, and `ProtocolVersion = 1` in the returned
|
||
parameters.
|
||
|
||
**Consequence for the client:** the mismatch check in Sections 6.2 and 9.1 is
|
||
the only line of defence and MUST be implemented. A client that assumes the
|
||
server will reject it will silently run a test against an incompatible peer.
|
||
|
||
### 19.14 Reply Length Longer Than Negotiated — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64. Extended 2026-08-11
|
||
with a negotiated-length sweep and a version comparison.
|
||
|
||
**Result:** Yes, in one specific, verified case. When midpoint timestamps are
|
||
negotiated with a single clock (wall only or monotonic only), upstream 0.9.1
|
||
servers emit both midpoint fields on the wire — wall field then monotonic field —
|
||
which is one 8-byte field more than the negotiated parameters imply. Measured
|
||
across all combinations of received-stats and clock with midpoint timestamps;
|
||
confirmed at the application layer by an upstream client reporting 36-byte
|
||
packets while receiving 44-byte datagrams, and completing normally.
|
||
|
||
**The excess is bounded by the ordinary length rule, not constant.** Reply length
|
||
is the larger of the negotiated length and the field block; the extra field
|
||
enlarges only the field block. Measured with received-stats = both, midpoint,
|
||
wall (`normal_header` 36, `upstream_header` 44): negotiated lengths 0, 16, 32,
|
||
36 → 44-byte replies; 37 → 44 (+7); 40 → 44 (+4); 43 → 44 (+1); 44 → 44 (0);
|
||
45, 64, 1024, 4096 → exactly the negotiated length (0). At 4096 the midpoint
|
||
region still contains both fields — the extra field displaces payload rather than
|
||
extending the datagram. Any claim that upstream replies are simply "the
|
||
negotiated length plus 8" is wrong outside the first regime. Each cell of that
|
||
sweep is a different negotiation with a single reply length; the +7 / +4 / +1
|
||
figures are differences between negotiations, never alternative lengths available
|
||
within one.
|
||
|
||
**Scope of the requirement.** For single-clock midpoint the client MUST accept a
|
||
reply at **exactly** `upstream_0_9_1_reply_len`. Where that value exceeds
|
||
`compatible_reply_len` the form is identifiable by length alone and the two
|
||
values differ by between 1 and 8 bytes — but that is one additional accepted
|
||
length, not a `+1..+8` window, and a length strictly between the two MUST be
|
||
rejected like any other unexpected length. That is the whole of the tolerance: it
|
||
does **not** generalise to accepting arbitrary overlong echo replies, and the
|
||
client MUST continue to reject replies *shorter* than `compatible_reply_len` —
|
||
the normal compatible reply length, not merely the negotiated `Length` value —
|
||
and to validate every other unexpectedly sized datagram strictly. It MUST NOT
|
||
require the extra field to be present.
|
||
|
||
**The monotonic-only ambiguity.** Once the negotiated length reaches
|
||
`upstream_header`, the upstream and conforming forms are the same size and are not
|
||
distinguishable from the datagram alone. A client MUST NOT unconditionally read
|
||
the second 8-byte region as the monotonic value whenever midpoint + monotonic is
|
||
negotiated — against a conforming server that region is ordinary payload. Apply a
|
||
correction only when the dual-field form is otherwise identifiable, meaning
|
||
`upstream_0_9_1_reply_len > compatible_reply_len` and the datagram is exactly
|
||
that length; otherwise record the value as ambiguous, and accept the packet
|
||
either way. See the server specification, Section 11.3.1.
|
||
|
||
A server that emits only the negotiated fields is also fully interoperable: a
|
||
server built to do so was driven by the upstream 0.9.1 client for wall-only,
|
||
monotonic-only and dual-clock midpoint configurations and every run completed
|
||
cleanly. Upstream 0.9.0 behaves that way on the wire; the dual-field form appears
|
||
in 0.9.1 and is unchanged in the tested post-0.9.1 tree.
|
||
|
||
### 19.15 Received Window After Reordering or a Large Gap — ✅ RESOLVED
|
||
|
||
**Status:** Verified 2026-08-11, irtt 0.9.1, macOS arm64.
|
||
|
||
**Result:** The window is **reset to `0x1`**, not merely left unshifted, in both
|
||
of these cases:
|
||
|
||
- a request whose sequence number is lower than the highest already seen
|
||
(reordering, or a late gap fill);
|
||
- a request whose sequence number is 64 or more ahead of the previous one.
|
||
|
||
The count still increments. A gap of exactly 63 preserves the older packet in
|
||
bit 63; a gap of 64 discards everything.
|
||
|
||
The window value 0 was never observed; bit 0 is always set. 32-bit sequence
|
||
wraparound is handled as ordinary modular arithmetic and does not disturb the
|
||
window.
|
||
|
||
Full input → output tables are in
|
||
`test-vectors/SERVER_BEHAVIORAL_VECTORS.md` Section 1, with a capture in
|
||
`captures/server-recv-window.pcapng`. See Section 12.8 for the classification
|
||
consequence.
|
||
|
||
---
|
||
|
||
## 20. Non-Goals
|
||
|
||
This specification does **not** require:
|
||
|
||
- Matching upstream source layout, file organization, or module structure.
|
||
- Matching upstream private names (functions, types, variables).
|
||
- Matching upstream internal algorithms where equivalent behavior can be
|
||
achieved independently.
|
||
- Matching exact CLI text output.
|
||
- Implementing the server.
|
||
- Implementing every optional upstream feature in the first milestone.
|
||
- Implementing timer compensation with any specific algorithm (only accurate
|
||
send timing is required).
|
||
- Implementing any specific concurrency model.
|
||
- Using any specific programming language features or libraries.
|
||
- Supporting IRTT protocol versions other than 1.
|
||
- Supporting the development-era (0.1.x) protocol.
|