Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started

udpstp measures how much traffic a network path can carry, in Mbps. Put a Client and a Server at the two ends of the path, share a key, and run your first Test.

Install udpstp

Install udpstp on both hosts using the installation guide.

Choose two hosts

Run the Server at the far end of the path you want to measure, and the Client where you want to test from. Both hosts need UDP connectivity. Testing on one machine checks the setup but doesn’t measure a network path.

Choose a MaxHops value slightly above the number of routers between the hosts; the examples below use 12.

Share a key

Tests are authenticated by default. Generate a random Shared Key with openssl rand -hex 32, or use the Windows instructions. Save the following as keys.toml on both hosts, replacing the placeholder with that same key:

[[key]]
admin_key_name = "first-test"
key_id = 1
key_hex = "replace-with-64-hexadecimal-digits"

Keep the file private. On Linux and macOS:

chmod 600 keys.toml

On Windows, set the key file’s permissions before continuing. Keys stay in the file, never on the command line.

Start the Server

Allow inbound UDP ports 24601 and 49152–65535 at the Server. Open the firewall covers other setups. On Windows, follow these steps before starting it.

As an ordinary account on the Server host:

udpstp server --key-file keys.toml --max-hops 12

Run your first Test

On the Client host, replace server.example.net with your Server’s name or address:

udpstp client --key-file keys.toml --max-hops 12 server.example.net

This runs a 10-second Downstream Test: the Server sends the traffic. Add --direction upstream to measure the other direction. The result shows the Maximum IP-Layer Capacity in Mbps and any warnings. Understanding the results explains what they mean.

On Windows, the Client normally needs no inbound firewall rule. A Server needs its own rule, added by an administrator.

Keep exploring

Guides

Start with Getting started for your first authenticated Test, then use these guides for the task at hand. Each guide gives the steps, and links to the explanations and reference pages for the details.

GuideForCovers
Installing udpstpClient users and Server administratorsInstalling release binaries, platforms, upgrades and packages
Running a TestClient usersKey files, Authentication Modes, Upstream and Downstream Tests, JSON results, Multi-Connection Tests and common problems
Hosting a ServerServer administratorsConfiguration, keys, the firewall, limits, MaxHops, socket buffers, address privacy, debugging, running it as a service, and the log
udpstp on WindowsWindows users of either rolePrivate key files, the firewall, a trial Server and the Windows service
Operator featuresOperatorsOperator Keys, Fixed-Rate Tests, long Test Intervals, MaxHops and DSCP
Building udpstpContributors, and users who need a source buildBuilding and installing from the repository

To understand the results, read Understanding the results. The security model explains the defaults, and Stable interfaces says what scripts can rely on.

Installing udpstp

Install udpstp on both Test hosts. Choose your platform below, then follow Getting started to share a key and run a Test.

Installation methods

PlatformMethodAvailability
LinuxArchive installer, signed apt and dnf repositoriesAvailable
Linux and macOSHomebrewAvailable
WindowsPowerShell installer and portable downloadAvailable
WindowsWinGetAwaiting catalog approval
All threeCargo source build and cargo-binstallAvailable

Linux

The installer selects a matching binary and places it in ~/.local/bin.

curl -fsSL https://udpstp.com/install.sh | sh

Run it again to update. It leaves your shell configuration unchanged. If ~/.local/bin is outside your PATH, add it using your shell’s configuration.

macOS

brew install pederbe/tap/udpstp

Use brew upgrade udpstp for updates and brew uninstall udpstp to remove it.

Windows

In PowerShell

irm https://udpstp.com/install.ps1 | iex

The installer uses your account’s LocalAppData\Programs\udpstp folder and adds it to PATH. You can use udpstp in the current PowerShell window immediately. Sign out and back in to refresh PATH for other applications. The Windows guide covers key-file permissions, firewall rules and running the Server as a service.

WinGet catalog approval is pending. The PowerShell installer and portable download are available now.

Signed Linux packages

The apt and dnf repositories provide normal package-manager updates and the Server’s systemd unit. The service stays disabled until you configure it and requires systemd 247 or later. Hosting a Server covers configuration and startup.

Debian and Ubuntu

Add the repository once, then install udpstp

curl -fsSL https://packages.udpstp.com/udpstp.gpg | sudo tee "/usr/share/keyrings/udpstp.gpg" >/dev/null
echo "deb [signed-by=/usr/share/keyrings/udpstp.gpg] https://packages.udpstp.com/apt stable main" | sudo tee /etc/apt/sources.list.d/udpstp.list
sudo apt update
sudo apt install udpstp

Updates arrive with apt upgrade. Remove the package with sudo apt remove udpstp.

Fedora, RHEL and similar distributions

sudo curl -fsSLo /etc/yum.repos.d/udpstp.repo https://packages.udpstp.com/udpstp.repo
sudo dnf install udpstp

Updates arrive with dnf upgrade. Remove the package with sudo dnf remove udpstp.

Portable downloads

The release page provides archives for the supported targets. Extract an archive and run its udpstp executable. Platform differences explains the available features.

Cargo

With Rust installed, build a release from source

cargo install --locked udpstp

With cargo-binstall installed, fetch a matching prebuilt release

cargo binstall udpstp

Building from source covers compiler and platform prerequisites.

AUR and Scoop recipes remain available as sources for later catalog submissions.

Running a Test

For Client users: Operators, measurement platforms, and anyone who runs udpstp client against a UDPSTP Server. Understanding the results explains what the results mean. udpstp client --help lists every option.

Get a key file

Tests are authenticated, so you need the same Shared Key as the Server. Ask the Server’s administrator for the key and its keyId.

  1. Save them in a key file. A name ending in .toml is a TOML Key Table:

    [[key]]
    admin_key_name = "customers-2026"
    key_id = 1
    key_hex = "5b0c0e8d6f7a41c2b9d3e4f5061728394a5b6c7d8e9fa0b1c2d3e4f506172839"
    

    Give the key as key_hex (hexadecimal) or key_text (its UTF-8 bytes), not both. Any other file name is read as an OB-UDPST udpst.keys file, with one <keyId>,<key> line per key.

  2. Make the file private. On Linux and macOS:

    chmod 600 keys.toml
    

    On Windows, follow Key file permissions. udpstp refuses a key file others can read, with exit status 51.

If the file holds several keys valid now, choose one with --key-id. Key files describes the format in full.

Run a Test

Run a 10-second Downstream Test, in which the Server sends the load:

udpstp client --key-file keys.toml server.example.net

Give the Server as a host name or address, with a port if it isn’t the Standard Port, 24601: server.example.net:24601, 192.0.2.10 or [2001:db8::10]:24601.

Run an Upstream Test, in which the Client sends the load:

udpstp client --key-file keys.toml --direction upstream server.example.net

Choose the Authentication Mode

You’re testing againstUse
a udpstp Server, or any Server that supports mode 2nothing extra: mode 2 is the default and authenticates every Status PDU too
an OB-UDPST Server--auth-mode 1, since OB-UDPST 9.1 doesn’t support mode 2
several Servers, some without mode 2--mode-1-server SERVER for each of those
a Server that allows only Unauthenticated Mode--unauthenticated, without a key file

The Client never falls back from mode 2 to mode 1 by itself, so an attacker can’t downgrade a Test. When a Server seems not to support mode 2, the Client says so:

udpstp: SERVER probably doesn't support mode 2: it rejected it, unverified; use --auth-mode 1 (the Client never falls back by itself)

Use --unauthenticated only on networks where every host is trusted. It protects nothing, and the Server must allow it too.

Common options

OptionWhat it doesDefault
--direction upstreamRun an Upstream Testdownstream
--test-interval SECONDSThe Test Interval, 5 to 60 s, or longer with an Operator Key that allows it (Operator features)10
--max-bandwidth MBPSThe most the Test is expected to carry. The Server searches no higher, and a Server with an Admitted Rate Cap may require itnone
--start-row ROWStart the search at this Sending Rate Table row. Row N offers N Mbps up to row 1000the Server’s choice
--test-connections NRun a Multi-Connection Test with N Test Connectionsone per SERVER
--algorithm cSearch with algorithm C (TR-471) instead of Bb
--delay-var rttJudge by round-trip delay variation instead of one-way, which follows clock driftone-way
--random-payloadFill Load PDUs with random bytes, so compression on the path can’t inflate the resultzeros
--max-loss-ratio RATIOThe Performance Criterion: the most a Sub-Interval may lose and still count0.01
--refuse-coercion PARAMETEREnd the Test if the Server changes this Activation parameter, named as in RFC 9946, such as testIntTime. Repeatableaccept the change
--format jsonPrint JSON results (below)text
--max-hops HOPSMaxHops: the TTL or hop limit of this end’s Test traffic64
--dscp DSCPMark the Test’s traffic with this DSCP. The Server may change it to 00

--jumbo and --traditional-mtu must match the Server’s settings, or it refuses the Test.

Read the results

The Client prints its progress, then the results:

Sending the Setup Request to 192.0.2.10:24601
Setup accepted, Ephemeral Port 56843
Test activated (Downstream)
The Server ended the Test

The headline is the Maximum IP-Layer Capacity near the end of the results:

  Maximum IP-Layer Capacity 1809.98 Mbps in Sub-Interval 8 (Loss Ratio 0.000000, …

Any warnings follow it under Warnings. A Test that completed with warnings still exits with status 0, so read them: each one says what limited the result. Fix common problems says what to do about each, and Understanding the results explains every line.

When the Server changes a parameter you asked for, a line says so before the Test starts, and the Test runs with the Server’s value:

The Server set testIntTime to 60 (asked for 120)

To end the Test instead, add --refuse-coercion testIntTime.

Save JSON results

udpstp client --key-file keys.toml --format json server.example.net > result.json

The JSON follows OB-UDPST’s schema, so tools written for OB-UDPST keep working. Progress and warnings go to standard error, so standard output holds only the JSON. A last "udpstp" object adds what OB-UDPST’s schema lacks. Leave it out with --json-extension=false for tools that reject unknown keys.

In udpstp’s JSON, AtMax and ModalResult are the Maximum IP-Layer Capacity, which meets the Performance Criterion, not OB-UDPST’s highest Sub-Interval. JSON results lists every field.

To keep your own address out of shared results, add --address-privacy truncate or --address-privacy pseudonymize --address-privacy-key-file FILE.

Run a Multi-Connection Test

One Test Connection may not fill a fast path. Run several in the same direction, and the Client sums their results:

udpstp client --key-file keys.toml --test-connections 4 server.example.net
  • --test-connections N runs 1 to 255 Test Connections. OB-UDPST Servers accept at most 24, and so does a udpstp Server by default.
  • Several SERVERs share the Test Connections in turn. They must all be IPv4 or all IPv6.
  • --min-test-connections N sets how many must start for the Test to go on. By default all of them must.
  • --max-bandwidth is for the whole Test, shared equally between its Test Connections.

If the Server answers from another address

The Client accepts responses only from the address it sent to. If a multi-homed Server answers from another one, ask its administrator for that address and allow it:

udpstp client --key-file keys.toml --allow-server-address server.example.net=192.0.2.20 server.example.net

The SERVER before = must match the one you test against exactly, port included. Repeat the option for more addresses. Unauthenticated Tests can’t use it.

Stop a Test

Press Ctrl-C. The Client tells the Server to stop and prints the results measured so far. A second Ctrl-C ends the Client at once. Exit statuses says how an interrupted Test exits.

Fix common problems

No answer from the Server

udpstp: no answer from 192.0.2.10:24601 within 3 s

A Server doesn’t answer a request that fails authentication, so a wrong key, a wrong keyId and a blocked port all look the same. Check, in order:

  1. the SERVER address and port;
  2. firewalls between you and the Server;
  3. the key and keyId;
  4. that both clocks are within 5 s of true time.

If it still fails, ask the Server’s administrator to turn on Troubleshooting Mode for a while. The Client then shows the reason:

udpstp: an unverified Setup Response said: authentication failed; check the key and keyId

If you use --checksum and the Client reports that a response failed its header checksum, try without --checksum.

The Server refused the Test

StatusResponseWhat to do
102BADVERUse a Client and Server that speak the same protocol version
103, 111BADJS, BADTMTUMatch the Server’s --jumbo or --traditional-mtu setting
109NOMAXBWSet --max-bandwidth
110CAPEXCRetry later, or ask for less with --max-bandwidth
113CONNFAILRetry later. The Server is full, or you have reached its limit of Tests or your Quota. A second Test from your address while one runs gets this too
152BADPARAMOften a Fixed-Rate Test without an Operator Key, or a Test that no longer fits the Server’s limits

Other exit statuses

StatusWhat to do
50Fix the command line, or give a key file or --unauthenticated
51Fix the key file. The message names the problem and the line
75Check the SERVER name and your network
107, 108, 157, 158Check the key, the keyId and both clocks
200See No answer from the Server
201Ask for a value the Server allows, or drop --refuse-coercion
202Report it, with both ends’ versions
203Use Servers with the same configuration for one Multi-Connection Test

Exit statuses lists them all.

Warnings

WarningWhat to do
No Sub-Interval met the Performance CriterionCompare the Highest IP-Layer Capacity and the Loss Ratios. A bottleneck with a shallow buffer loses a few percent all along, and --max-loss-ratio 0.1 accepts that
A Sub-Interval before or after the Maximum’s ran faster, or the last one much slowerLook at the Loss Ratios after the Maximum’s Sub-Interval. A few percent at a steady capacity is the search cycling around the bottleneck, and --max-loss-ratio 0.05 accepts it. Through CAKE, use enough --test-connections that each carries under 1 Gbit/s. If the path slowed, test again
Load PDUs not sent: this host fell behind (Skipped Bursts)Use a faster host, or fewer Test Connections per host
Load PDUs not sent: the send buffer was full (Withheld Bursts)Raise the socket buffers
The Server sent below its Fixed-Rate rowTest against a less loaded Server or at a lower row, or ask its administrator to check its log
The search never confirmed Congestion: the maxBandwidthRaise --max-bandwidth, or leave it out
The search never confirmed Congestion: the top of the Sending Rate TableUse more --test-connections
The search never confirmed Congestion: Hold IntervalsTest over a steadier path. A busy Wi-Fi link’s delay varies too much for the search to rise
The search never confirmed Congestion: still risingUse a longer --test-interval, or --algorithm c, which rises faster
The search never confirmed Congestion: a Load Sender stopped risingIf it was the Server’s, test against a less loaded Server. If it was this host’s, as for Skipped Bursts
Datagrams too large for the path with DF setDrop --traditional-mtu, or ask the Server’s administrator to lower its server_mtu
ICMP time-exceeded errors: –max-hops is too small for the pathRaise --max-hops above the path length
Status PDUs never arrived, or the peer had missed Status PDUsLook for loss in the reverse direction
Status PDUs failed authentication and were ignoredSomething on the path changed or injected them
Nothing was received for 1 s, or the Watchdog ended the TestCheck the path and the Server
Load PDUs without a delay sample: the wall clock steppedLet NTP settle before testing

What the warnings mean explains each one.

The OS held the socket buffers below the size asked for

At high rates, small socket buffers drop datagrams on the host, which looks like loss on the path. udpstp asks for 2 MiB and warns when the OS grants less:

udpstp: warning: the OS holds socket buffers below the 2097152 bytes asked for (receive …, send …); at high rates datagrams may be dropped locally, so raise the net.core.rmem_max and net.core.wmem_max sysctls

On Linux, raise the limits, then check the “This host” line of the next Test:

sudo sysctl -w net.core.rmem_max=4194304 net.core.wmem_max=4194304

On macOS and FreeBSD the limit is kern.ipc.maxsockbuf.

Hosting a Server

For Server administrators: the steps to set up, open, limit and run udpstp server. Server configuration lists every setting, Server log every event, and the security model explains the defaults. On Windows, follow udpstp on Windows alongside this guide.

The required setup gets a Server running. A Server that Clients outside your own network can reach should also take the recommended steps. Each optional step says when you need it.

Required setup

Every Server needs these four steps, in this order. Install udpstp first, as in Installing udpstp.

Add keys

The Server authenticates Clients with its Key Table. It reads the table once at startup, so restart the Server after you change it.

  1. Generate a random Shared Key of at least 32 bytes:

    openssl rand -hex 32
    

    On Windows, use PowerShell.

  2. Write a TOML Key Table with one [[key]] row per key:

    # Consumers: one key per population of Clients.
    [[key]]
    admin_key_name = "customers-2026"
    key_id = 1
    key_hex = "5b0c0e8d6f7a41c2b9d3e4f5061728394a5b6c7d8e9fa0b1c2d3e4f506172839"
    accept_lifetime_end = "20270101000000Z"
    send_lifetime_end = "20270101000000Z"
    tests_per_quota_period = 12
    
    # Next year's key, added in advance so no restart is needed.
    [[key]]
    admin_key_name = "customers-2027"
    key_id = 3
    key_hex = "0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f0"
    send_lifetime_start = "20261231000000Z"
    accept_lifetime_start = "20261231000000Z"
    
    # The NOC's Operator Key.
    [[key]]
    admin_key_name = "noc"
    key_id = 200
    key_hex = "c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4"
    operator = true
    max_test_interval = 3600
    

    Don’t use these example keys. Key files describes every field.

  3. Make the file private. On Linux and macOS it must be mode 0600 or stricter, owned by the account running udpstp or by root, in a folder only its owner can write:

    chmod 600 keys.toml
    

    On Windows, follow Key file permissions.

  4. Give each Client population its key and keyId over a secure channel.

Use a separate key for each population of Clients, and separate keys for Operators. To retire a key on schedule, set its accept_lifetime_end and send_lifetime_end. To revoke it at once, remove its row and restart the Server, which also ends the Tests running then.

Open the firewall

TrafficPortSetting
Setup Requests and ResponsesUDP 24601, the Standard Portport
Each Test Connection’s trafficOne Ephemeral Port each, picked at randomephemeral_ports, 49152-65535 by default

What to open depends on the firewall:

  • A stateful firewall on the Server’s host or in a cloud security group needs only the Standard Port open inbound.
  • A stateless filter, such as a router ACL, needs the Standard Port and the whole ephemeral_ports range.
  • A Server behind NAT needs the Standard Port and the whole range forwarded to it, with the port numbers unchanged.
  • At high packet rates, you may prefer to exempt Test traffic from connection tracking (nftables notrack) and allow the range as for a stateless filter.
  • Windows Defender Firewall needs the whole range. See udpstp on Windows.

To keep a firewall rule small, narrow the range. It must hold at least max_test_connections plus the Unactivated Test Connection share, 384 ports with the defaults:

udpstp server --key-file keys.toml --ephemeral-ports 50000-50383 --max-hops 12

To listen on specific addresses instead of all of them, set listen (or repeat --listen ADDRESS).

Set MaxHops

RFC 9946 says the operator MUST set MaxHops, the TTL or hop limit of Test traffic, so that load can’t stray beyond the intended path. Until you set max_hops, the Server uses 64 and warns at every start:

udpstp: warning: max_hops (--max-hops) is not set, so test traffic has a TTL or hop limit of 64; RFC 9946 §10 says the operator MUST set it from the expected path length

Set it a little above the number of hops to your farthest Clients, which traceroute shows. Clients set their own with --max-hops.

Keep dont_fragment on (the default), so a packet larger than the Path MTU is lost rather than fragmented. Turn it off only to measure a path with fragmentation.

Start the Server

Run the Server with your key file and MaxHops:

udpstp server --key-file /etc/udpstp/keys.toml --max-hops 12

The Server listens on UDP 24601, the Standard Port, on every IPv4 and IPv6 address, and runs in the foreground. It needs no root privileges or capabilities.

Without a key file the Server can admit no Test, and warns at startup:

udpstp: warning: no key file and Unauthenticated Mode is off, so the Server can admit no Test; give one with --key-file, or allow unauthenticated Tests

For a Server that Clients outside your own network can reach.

Limit what Clients can use

A Server open to the internet should cap the load it agrees to carry. The defaults already allow one Test at a time per Source (an IPv4 address or an IPv6 /64) and budget Setup Requests.

  1. Cap the admitted rate. Set admitted_rate_cap, in Mbps for each direction, together with require_max_bandwidth. Without it, Tests that state no maxBandwidth don’t count against the cap, and the Server warns:

    udpstp: warning: admitted_rate_cap is set but require_max_bandwidth is off: Tests that state no maxBandwidth are admitted without counting
    
  2. Set Quotas if you want to limit how often each Source or key tests. Quotas are off by default and refill evenly over the quota_period (3600 s by default):

    udpstp server --key-file keys.toml --max-hops 12 --source-tests-per-quota-period 24
    

    Set a key’s own Quotas with tests_per_quota_period and bytes_per_quota_period in its row. Operator Keys are exempt. Use is counted in memory, so a restart forgets it.

  3. Give each population of Clients its own key, and keys only to Clients you trust. The Server shares its capacity fairly between keys under a flood, but it can’t tell one key’s Clients apart.

Keep the other limits at their defaults unless you have a reason to change them. Server configuration lists each one, and the security model explains how they hold up under a flood. A Client refused by a limit sees CONNFAIL, CAPEXC or NOMAXBW and exits with status 113, 110 or 109.

Protect Client addresses

The log shows each Client’s address in full by default, for abuse handling. Client addresses are personal data in many jurisdictions. Set address_privacy to show less:

ValueThe log shows
fullThe address and port, such as 127.0.0.1:51801
truncateThe IPv4 /24 or IPv6 /48, without the port
pseudonymizeanon- and 8 bytes of HMAC-SHA-256 over the IP address, such as anon-63d4269f2e4b0c0d

pseudonymize needs a key in address_privacy_key_file: hexadecimal digits for 16 bytes or more. A Client keeps the same pseudonym for as long as you keep the key:

openssl rand -hex 32 > /etc/udpstp/pseudonym.key
udpstp server --key-file keys.toml --max-hops 12 --address-privacy pseudonymize --address-privacy-key-file /etc/udpstp/pseudonym.key

Run it as a service

Whatever runs the Server, give it:

  • a dedicated, unprivileged account that alone can read the key file;
  • core dumps disabled, for example with ulimit -c 0, since a dump can hold the Key Table;
  • a log system that rotates its standard output;
  • a clock synchronised with NTP, since clocks more than 5 s apart make every authenticated Test fail.

With systemd

The supplied unit requires systemd 247 or later for credential loading.

contrib/systemd/udpstp-server.service is a hardened unit. It runs the Server as a user systemd creates when it starts, with no capabilities and a read-only view of the system. It loads the Key Table as a credential and makes a mode-0400 copy in its private runtime directory. The source in /etc/udpstp stays root’s, and the runtime copy is removed when the unit stops. The apt and dnf packages install it for you.

sudo install -m 755 target/release/udpstp /usr/local/bin/udpstp
sudo install -d -m 755 /etc/udpstp
sudo install -m 600 keys.toml /etc/udpstp/keys.toml
sudo install -m 644 server.toml /etc/udpstp/server.toml
sudo install -m 644 contrib/systemd/udpstp-server.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now udpstp-server
journalctl -u udpstp-server -f

server.toml is your configuration file. Leave key_file out of it, since the unit passes the key file itself. To bind a port below 1024, add AmbientCapabilities=CAP_NET_BIND_SERVICE and CapabilityBoundingSet=CAP_NET_BIND_SERVICE.

In a container

Each release publishes the image ghcr.io/pederbe/udpstp:<version>, which runs the Server as a non-root user (uid 65532) with no shell. Pin a version and upgrade on purpose. To build the image yourself:

podman build -f contrib/container/Containerfile -t udpstp .

Run it with the host’s network (--network host). A container network puts NAT and a bridge in front of the Server, so results would describe the container’s plumbing rather than the path. Pass the Key Table as a secret owned by uid 65532:

podman secret create udpstp-keys keys.toml
podman run -d --name udpstp --network host \
    --secret udpstp-keys,target=keys.toml,uid=65532,mode=0400 \
    ghcr.io/pederbe/udpstp:<version> --max-hops 16

Arguments after the image name are added to udpstp server’s and override the image’s configuration file, /etc/udpstp/server.toml. Or mount your own file there. With Docker, pass the key file as a Compose secret, or bind-mount a file owned by uid 65532 with mode 0400. podman stop and docker stop stop the Server cleanly within 2 s.

On Windows

Run an unattended Server as the Windows service udpstp-server. See Run the Server as a service.

Optional steps

Each step says when you need it. Skip the rest.

Write a configuration file

When: you have more than a few settings, or want them kept in one file.

Every setting has a command-line flag and a TOML key of the same name: --max-hops is max_hops. Flags replace the file’s values, and the file replaces the defaults.

  1. Print the effective configuration to start a file:

    udpstp server --print-config > /etc/udpstp/server.toml
    

    Or write only what differs from the defaults:

    listen = ["192.0.2.10", "2001:db8::10"]
    key_file = "/etc/udpstp/keys.toml"
    ephemeral_ports = "50000-50383"
    max_hops = 12
    admitted_rate_cap = 10000
    require_max_bandwidth = true
    source_tests_per_quota_period = 24
    log_format = "json"
    address_privacy = "truncate"
    
  2. Use absolute paths. A relative key_file is taken from the directory the Server starts in, not the file’s.

  3. Check the file, keys included:

    udpstp server --config /etc/udpstp/server.toml --print-config
    
  4. Start the Server with it:

    udpstp server --config /etc/udpstp/server.toml
    

An unknown key or an invalid value stops the Server with exit status 50 and names the setting:

udpstp: configuration error: ephemeral_ports: "50000-50099" has 100 ports, fewer than max_test_connections (256) plus the Unactivated Test Connection share (128)

Keep jumbo, traditional_mtu and server_mtu at their defaults to serve OB-UDPST Clients as well as udpstp ones. A Client whose --jumbo or --traditional-mtu differs is refused with BADJS or BADTMTU.

Raise the socket buffers

When: the Server warns that the OS holds socket buffers below the size asked for.

At high rates, small socket buffers drop datagrams on the host, which looks like loss on the path. The Server asks for socket_buffer_size, 2 MiB by default, and warns at startup when the OS grants less:

udpstp: warning: the OS holds socket buffers below the 2097152 bytes asked for (receive …, send …); at high rates datagrams may be dropped locally, so raise the net.core.rmem_max and net.core.wmem_max sysctls

On Linux, raise the limits to at least socket_buffer_size, and make the change permanent in a file under /etc/sysctl.d/:

sudo sysctl -w net.core.rmem_max=4194304 net.core.wmem_max=4194304

On macOS and FreeBSD the limit is kern.ipc.maxsockbuf.

Give it enough CPUs

When: the Server carries high rates or many Tests at once.

On Linux the Server runs one Engine Thread for each CPU it may use, each serving many Test Connections. Give it whole CPUs, and confine it with the service’s CPU affinity, such as systemd’s CPUAffinity= or taskset. A container’s fractional CPU limit is rounded down.

For high-rate Multi-Connection Tests, give the Server at least as many CPUs as such a Test has Test Connections.

A Server that falls behind says so in its TestSummary log lines: skipped_bursts when it couldn’t send on time, and receive_buffer_overflows for its Receive Buffer Overflows.

Debug a failing Client

When: a Client gets no answer and you suspect its key.

By default a Setup Request that fails authentication gets no answer, so a Client with the wrong key sees only a timeout. To find out why, turn on Troubleshooting Mode for a while:

udpstp server --key-file keys.toml --max-hops 12 --troubleshooting

The Client then reports the reason:

udpstp: an unverified Setup Response said: authentication failed; check the key and keyId

Turn it off when you’re done. While it is on, anyone can learn whether the Server has keys, and spoofed requests get replies.

Allow Tests without a key

When: on a lab network where every host is trusted, for Clients without a key.

Only on a network where every host is trusted, such as a lab, set allow_unauthenticated to accept Tests without a key, as default OB-UDPST installations do. It protects nothing: anyone who can reach the Server can start a Test. The Server still accepts authenticated Tests.

Read the log

When: you want to follow Tests, or feed the log to another system.

The Server writes one line per event to standard output, human-readable by default. Set log_format = "json" for one JSON object per line:

2026-09-26T14:46:26.621011Z  INFO Test Connection admitted event="TestConnectionAdmitted" test_connection_id=1 client="127.0.0.1:51801" mc_ident=60798
2026-09-26T14:46:37.129734Z  INFO Test ended: Completed event="TestSummary" client=127.0.0.1:51801 mc_ident=60798 test_connections=1 direction="Downstream" test_interval=10 … result="Completed"

Filter on the event name and its fields, not on the message. Under a service manager, leave the log on standard output and let it rotate the log. Otherwise set log_file to write a new file each day, kept for 14 days, in a folder of its own. Server log lists every event, field and reason.

udpstp on Windows

The Windows steps for both roles: a private key file, the firewall, a trial Server in a window, and the Windows service. Use it alongside Running a Test and Hosting a Server. Platform differences lists what Windows can’t do yet.

Generate a Shared Key

PowerShell can generate a 32-byte key without OpenSSL:

$bytes = New-Object byte[] 32
$rng = [Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($bytes)
$rng.Dispose()
-join ($bytes | ForEach-Object { $_.ToString('x2') })

Put the output in key_hex, and copy the key file to the other host securely.

Key file permissions

Both roles refuse a key file that other accounts can read or change, with exit status 51 and a message naming the reason. Only the running account, SYSTEM and Administrators may have access.

For a Client, or a Server you run in a window:

  1. Put the key file in a folder of its own in your profile, holding only key files. Never use a folder created directly under C:\, where every signed-in account can change what’s inside.
  2. In Windows PowerShell 5.1, as the account that will run udpstp, go to that folder and run the following. For an OB-UDPST file, replace keys.toml with its name.
$ErrorActionPreference = 'Stop'
$keys = (Resolve-Path -LiteralPath 'keys.toml').Path
$folder = Split-Path -Path $keys -Parent
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
$trusted = "(A;;FA;;;$me)(A;;FA;;;SY)(A;;FA;;;BA)"
$sections = [Security.AccessControl.AccessControlSections]::Owner -bor [Security.AccessControl.AccessControlSections]::Access
$directory = New-Object Security.AccessControl.DirectorySecurity
$directory.SetSecurityDescriptorSddlForm("O:${me}D:P(A;OICI;FA;;;$me)(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)", $sections)
[IO.Directory]::SetAccessControl($folder, $directory)
$file = New-Object Security.AccessControl.FileSecurity
$file.SetSecurityDescriptorSddlForm("O:${me}D:P$trusted", $sections)
[IO.File]::SetAccessControl($keys, $file)
# Examine the actual file, not only its containing folder.
Get-Acl -LiteralPath $keys | Format-List Owner, AreAccessRulesProtected, AccessToString
icacls $keys
  1. Check the output. The file should be owned by your account, have inheritance protected, and list only SYSTEM, Administrators and your account, each with (F).

  2. Check that udpstp accepts it, with no key file error:

    udpstp server --key-file keys.toml --print-config
    

The script replaces the folder’s and the file’s permissions completely, removing any other account’s access. If other accounts need the folder, keep a copy elsewhere first. To add a key file later, copy it into the folder rather than moving it, since a moved file keeps its old permissions, and run the script again for it.

For an unattended Server, the service installer sets up its own protected folder instead.

Run a Client

The Client starts every exchange itself, so it normally needs no inbound firewall rule. A Server run from the same udpstp.exe still needs its own rule.

Try a Server in a window

For a trial, before the Server first starts:

  1. Install the program where only administrators can change it. As administrator, copy udpstp.exe into C:\Program Files\udpstp. The firewall rule names this path and lets in whatever program is there.

  2. Add the firewall rule, as administrator, before udpstp.exe first runs from that path. Allow your port and the whole ephemeral_ports range, here the defaults:

    $udpstp = "$env:ProgramW6432\udpstp\udpstp.exe"   # the udpstp.exe that runs the Server
    New-NetFirewallRule -DisplayName "udpstp Server" -Group "udpstp" `
        -Direction Inbound -Action Allow -Program $udpstp `
        -Protocol UDP -LocalPort '24601','49152-65535' -Profile Any
    

    For a Server that should answer only its own LAN, add -RemoteAddress LocalSubnet. To remove the rule later, run Remove-NetFirewallRule -Group udpstp.

  3. Protect the key file as above, for your own account.

  4. Start the Server from a window that isn’t elevated. Never use “Run as administrator”, which would give a network-facing process administrator rights:

    & "$env:ProgramW6432\udpstp\udpstp.exe" server --key-file keys.toml --max-hops 12
    

The log appears in the window. Ctrl+C stops the Server, and so does closing the window or signing out. Check that a Client on another host can run a Test, since a loopback Test doesn’t prove inbound access.

Avoid reserved ports

Windows can reserve ranges of ports, which the Server then can’t use. The Server skips them without a warning, so a range that overlaps them holds fewer Test Connections than it seems, and Clients get CONNFAIL. If you narrow ephemeral_ports, list the reserved ranges first and pick a range outside them:

netsh int ipv4 show excludedportrange protocol=udp
netsh int ipv6 show excludedportrange protocol=udp

If someone cancelled a firewall dialog

Without a rule for its path, Windows may ask whether to allow udpstp.exe when the Server starts. Allow, answered by an administrator, adds an Allow rule. Cancel adds Block rules, which override any Allow rule for that path, and Windows won’t ask again.

To recover, remove only the dialog’s Block rules for that path, as administrator, with $udpstp set as above:

Get-NetFirewallApplicationFilter | Where-Object Program -eq $udpstp |
    Get-NetFirewallRule | Where-Object { $_.Name -like '*Query User*' -and $_.Action -eq 'Block' } |
    Remove-NetFirewallRule

Then confirm that the remaining rules name the program and your UDP ports:

Get-NetFirewallApplicationFilter | Where-Object Program -eq $udpstp |
    Get-NetFirewallRule | Format-Table Name, Enabled, Direction, Action, Profile
Get-NetFirewallApplicationFilter | Where-Object Program -eq $udpstp |
    Get-NetFirewallRule | Get-NetFirewallPortFilter | Format-Table Protocol, LocalPort

This keeps rules an administrator created, and doesn’t override an organisation’s firewall policy.

Run the Server as a service

An unattended Server runs as the Windows service udpstp-server, under its own virtual account.

  1. As administrator, copy udpstp.exe to C:\Program Files\udpstp\udpstp.exe. The service runs whatever program is at that path, so it must be where only administrators can change it.

  2. Put your Key Table in C:\ProgramData\udpstp\keys.toml.

  3. From an elevated PowerShell, run the install script from the source’s contrib\windows folder:

    powershell -ExecutionPolicy Bypass -File .\udpstp-server.ps1 -Install
    

The script:

  • locks C:\ProgramData\udpstp to Administrators and SYSTEM, with read access for the service and write access to its logs folder;
  • writes a starting configuration, C:\ProgramData\udpstp\server.toml, if there is none;
  • checks the configuration, so a mistake shows up here rather than as a service that won’t start;
  • creates the service, running as NT SERVICE\udpstp-server with no privileges and no credential to set or expire;
  • adds the firewall rule udpstp-server for your port and ephemeral_ports;
  • starts the service once to check its keys, and only then sets it to start at boot and restart after a failure.

If a step fails, the script leaves the service stopped and disabled and reports why. Fix the problem and run -Install again.

To change the configuration, edit server.toml and run -Install again. Write paths in single quotes, such as 'C:\ProgramData\udpstp\keys.toml', since a backslash inside double quotes starts an escape. Save the file as UTF-8, not as the UTF-16 that Windows PowerShell 5.1’s > writes.

To upgrade, stop the service with Stop-Service udpstp-server, replace udpstp.exe, and start it again. The firewall rule stays valid because the path doesn’t change.

To uninstall, run the script with -Uninstall. It keeps C:\ProgramData\udpstp unless you add -RemoveData.

The log is in C:\ProgramData\udpstp\logs\server.<date>.log, a new file each day, with the last 14 kept. If the service won’t start, its log says why, and sc.exe query udpstp-server shows its exit status as SERVICE_EXIT_CODE: 50 for a configuration error, 51 for a key file error, 75 when it couldn’t start.

If the service fails with error 1069, a group policy has removed “Log on as a service” from NT SERVICE\ALL SERVICES. Grant it to NT SERVICE\udpstp-server.

Where PowerShell runs in Constrained Language mode, the script stops. Make the same changes by hand, following the list above.

With Fast Startup on, which client editions of Windows enable, “Shut down” hibernates the service instead of stopping it. Restart Windows to stop it.

Installation and firewall rules

Firewall rules name the program’s full path. The install script keeps udpstp.exe at the same path across upgrades, so its rules keep working. If you move the program, add a rule for the new path.

Scoop and winget run udpstp through a shim or a versioned folder. A rule for the shim doesn’t cover the program it runs, and a versioned path changes with each upgrade. For a Server, use the service above.

Operator features

For Operators measuring within their own networks, and for the Server administrators who give them Operator Keys.

Operator Keys

An Operator Key is a Shared Key that the Server’s Key Table marks as belonging to an Operator. The Server decides: a Test gets an Operator’s allowances only when its Setup Request verified with a row marked operator = true, whatever the Client claims.

On the Server, in a TOML Key Table (Key files):

[[key]]
admin_key_name = "noc"
key_id = 200
key_hex = "c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4"
operator = true
max_test_interval = 3600

max_test_interval is optional: without it, an Operator Key’s Tests are limited to 60 s like everyone else’s. An OB-UDPST udpst.keys file can’t mark Operator Keys.

On the Client, the key file needs only the key; operator there changes nothing:

[[key]]
admin_key_name = "noc"
key_id = 200
key_hex = "c3d2e1f00f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c4b5a69788796a5b4"

Give Operator Keys their own keyIds, and never share them with Consumers.

What an Operator Key allows

AllowanceWithout an Operator KeyWith one
Fixed-Rate Tests (--fixed-rate)Refused with BADPARAMAllowed
Test Interval (--test-interval)At most 60 s; a longer request is coerced to 60Up to the key’s max_test_interval
Starting row of a search (--start-row)At most max_start_row, row 100 by defaultAny row
Share of the Admitted Rate Cap one Test may stateAt most max_test_share, 50% by defaultUp to the whole cap
Quotas of Tests and load bytesCountedExempt
DSCPs the Test may mark its traffic withallowed_dscp, only 0 by defaultoperator_allowed_dscp, every DSCP but 48 and 56 (network control) by default
Time to infer Congestion (slowAdjThresh × trialInt)Lowered to 500 ms at mostKept as requested

The last row matters only to Clients that set slowAdjThresh or trialInt, which the udpstp Client doesn’t.

An Operator Key is not exempt from authentication, tests_per_source (one Test at a time per Source by default), the Test Connection limits, the Admitted Rate Cap itself, require_max_bandwidth, or the Setup Request budgets.

Fixed-Rate Tests

A Fixed-Rate Test holds one Sending Rate Table row for its whole Test Interval instead of searching. Use it to check that a path carries a known rate, for example a provisioned service rate.

udpstp client --key-file noc.toml --fixed-rate 100 server.example.net

Rows 1 to 1000 offer that many Mbps at the IP layer; rows 1001 to 1152 offer the rates above 1 Gbps. Row 0 offers about 0.1 Mbps. The results’ header names the Test type:

Results
  Downstream Fixed-Rate Test with 127.0.0.1:24701, started 2026-09-26T14:50:11Z (T0)

A Test with an ordinary key is refused:

udpstp: the Server rejected the Activation Request; only an Operator Key may run a Fixed-Rate Test (RFC 9946 §4.1), so check the key, or the Server's log

with exit status 152; the Server logs reason="FixedRateNeedsOperatorKey". --fixed-rate and --start-row can’t be combined.

Check the IP-Layer Sender Bit Rate. A Fixed-Rate Test sends only as fast as the Load Sender can. Compare the B column, the Sender Bit Rate, with the row’s rate. When it is below the row’s rate, the host, not the path, set the result, and the Server’s log shows BurstsSkipped.

Long Test Intervals

An Operator Key with max_test_interval allows Tests longer than 60 s, up to that many seconds:

udpstp client --key-file noc.toml --test-interval 900 --delay-var rtt server.example.net

Above 60 s the Server judges by round-trip delay variation instead of one-way delay variation, which clock drift would distort over a long Test. Ask for it with --delay-var rtt; otherwise the Server coerces it and the Client says so:

The Server set useOwDelVar to 0 (asked for 1)

A Test Interval beyond the key’s maximum is coerced to it. Add --refuse-coercion testIntTime to have the Client end the Test instead (exit status 201).

MaxHops

MaxHops, the TTL or IPv6 hop limit of test traffic, keeps a Test’s load from straying beyond the path you mean to measure (RFC 9946 §10). Each end sets its own: the Server with max_hops, which RFC 9946 says the operator MUST set, and the Client with --max-hops (64 by default).

udpstp client --key-file noc.toml --max-hops 12 server.example.net

Set it a little above the number of hops on the path. Too small, and the load dies on the way: the Client reports “ICMP time-exceeded errors: –max-hops is too small for the path” instead of a low capacity, and the Server logs it.

DSCP

To measure a specific traffic class, mark the Test’s traffic with its DSCP:

udpstp client --key-file noc.toml --dscp 46 server.example.net

The Server allows a DSCP from its operator_allowed_dscp set for an Operator Key, and from allowed_dscp for anyone else. A DSCP outside the set is coerced to 0. The results’ parameters line says which DSCP was agreed, such as DSCP 46 agreed.

A Server administrator narrows the Operator set with, for example:

udpstp server --key-file keys.toml --max-hops 12 --operator-allowed-dscp 0,10,18,26,34,46

The ECN bits are always Not-ECT. Windows doesn’t let udpstp mark traffic (Platform differences).

Multi-Connection Tests at high rates

One Test Connection may not fill a multi-gigabit path. Spread the Test over several Test Connections, and over several Servers if one host can’t send enough:

udpstp client --key-file noc.toml --test-connections 8 --max-bandwidth 10000 server-a.example.net server-b.example.net

Running a Test explains the options, and Understanding the results how they are summed.

Building udpstp

Build udpstp from source to develop it, try changes on main, or use a platform without a release binary. To install a published release, see Installing udpstp.

You need Git and Rust installed through rustup. The repository’s rust-toolchain.toml selects the toolchain; Cargo uses the checked-in dependency versions with --locked.

git clone https://github.com/pederbe/udpstp
cd udpstp
cargo build --locked --release -p udpstp

The binary is target/release/udpstp, or target\release\udpstp.exe on Windows. Run it by that path, or install it into Cargo’s binary directory so the guides’ udpstp commands work:

cargo install --locked --path crates/udpstp
udpstp --version

If Cargo says its binary directory isn’t on your PATH, add that directory using your shell’s settings. Then follow Getting started on both hosts.

For development checks and contributions, see CONTRIBUTING.md.

Understanding the results

What udpstp measures, what each line of its results means, and why a Test can fall short of a path’s real capacity. Running a Test has the steps.

What a Test measures

A Test sends load across the path for its Test Interval, 10 s by default, split into Sub-Intervals of 1 s. In a Search Test, the Server’s Load Rate Adjustment Algorithm raises the sending rate until the path shows loss or growing delay, then holds it around the limit.

Each Sub-Interval has an IP-Layer Capacity: the bits that arrived, IP and UDP headers included, over its length. The Maximum IP-Layer Capacity is the highest IP-Layer Capacity of the Sub-Intervals that meet the Performance Criterion, which caps the share of Load PDUs a Sub-Interval may lose (1% by default). It is the headline result, as RFC 9097 defines it.

The Highest IP-Layer Capacity is the highest of all Sub-Intervals, whatever they lost. OB-UDPST reports it as its maximum. udpstp shows it beside the Maximum whenever they differ.

The text results

This Downstream Test ran over loopback, so the two hosts, not a network, set its rates:

Test outcome: completed
Endpoints
  Test Connection 0: Client unknown; Server 127.0.0.1:24701
Results
  Downstream Search Test with 127.0.0.1:24701, started 2026-09-26T21:51:34Z (T0)
  Test Interval (I) 10 s in 10 Sub-Intervals (dt) of 1000 ms; Trial Interval
    (FT) and st 50 ms; 1 Test Connection (F); MaxHops 64; DSCP 0 agreed
  Performance Criterion: loss at most 1% of the Load PDUs sent, or one; Tmax up
    to 1000 ms (a late Load PDU counts within the Sub-Interval its gap was seen
    in)

  Sub-Interval results
  SI = Sub-Interval; C = IP-Layer Capacity; DV = delay variation (min/avg/max);
    RTTV = RTT variation (min-max, avg); - = no measurement.
  B = IP-Layer Sender Bit Rate.
  SI  C (Mbps)  Loss Ratio
   1    103.97    0.000000
   2    303.93    0.000000
   3    504.06    0.000000
   4    648.99    0.000000
   5    801.02    0.000000
   6   1450.50    0.000000
   7   1689.22    0.002149
   8   1809.98    0.000000
   9   1867.22    0.014783
  10   1860.05    0.015516

  SI  Lost  Reordered  Duplicates
   1     0          0           0
   2     0          0           0
   3     0          0           0
   4     0          0           0
   5     0          0           0
   6     0          0           0
   7   307          0           0
   8     0          0           0
   9  2340          0           0
  10  2443          0           0

  SI  DV (ms)  RTTV (ms)
   1    0/1/3     0-1, 0
   2    0/1/2     0-2, 1
   3    0/1/2     0-2, 1
   4    0/1/7     1-7, 2
   5    0/1/8     1-3, 1
   6   0/2/11     1-6, 2
   7   0/4/11     1-8, 4
   8    0/3/9     1-8, 3
   9   0/4/14     1-8, 4
  10   0/5/12     1-8, 5

  SI  B (Mbps)*
   1      93.75
   2     293.58
   3     493.68
   4     646.35
   5     789.84
   6    1432.82
   7    1665.09
   8    1810.14
   9    1893.99
  10    1893.41

  * The IP-Layer Sender Bit Rate estimated here, not measured by the Load
    Sender: the Load PDUs the Server sent in each st of its own clock, from
    their lpduSeqNo span, a lost one as big as those around it

  Summary
  F = Test Connections; SI = Sub-Interval; Max = Maximum IP-Layer Capacity; - =
    no valid result.
   Phase  F  SI  Max (Mbps)  Loss Ratio
  Search  1   8     1809.98    0.000000
  RTT minimum (ms)  RTT maximum (ms)
                 0                 8
  Maximum Sub-Interval result received at an unknown time
  Maximum IP-Layer Capacity 1809.98 Mbps in Sub-Interval 8 (Loss Ratio 0.000000,
    RTT variation 1-8 ms above a minimum of 0 ms, estimated IP-Layer Sender Bit
    Rate 1810.14 Mbps); Highest IP-Layer Capacity 1867.22 Mbps in Sub-Interval 9
    (Loss Ratio 0.014783)
  RTT minimum 0 ms
  Minimum one-way delay 0 ms (clockDeltaMin; valid only if the clocks are
    synchronized)

  This host: DF clear, socket buffers receive 2097152 and send 2097152 bytes;
    portable Data Phase Engine, no fast paths
Test completed

Line by line:

  • Test outcome: completed, completed with warnings or qualified results, partially completed, failed, aborted or interrupted. A Multi-Connection Test also gives each Test Connection’s own outcome.
  • Endpoints: each Test Connection’s Server and, where the socket knows it, the Client’s address. A socket bound to every address can’t name the Client and says unknown. --address-privacy applies here too.
  • Header: the direction, whether the algorithm set the rate (a Search Test) or an Operator fixed it (a Fixed-Rate Test), the Server, and the start time T0 in UTC.
  • Parameters, in RFC 9097’s notation: the Test Interval I, its Sub-Intervals of length dt, the Trial Interval FT, st (the interval the IP-Layer Sender Bit Rate is taken over), and F, the number of Test Connections. MaxHops is this end’s own setting. The DSCP is the one the Server agreed to.
  • Performance Criterion: the share of Load PDUs a Sub-Interval may lose and still count, set with --max-loss-ratio. One lost Load PDU always passes. A Sub-Interval in which nothing arrived never does. Tmax is how late a Load PDU may arrive and still count as received.
  • The tables, one row per Sub-Interval (SI). The terminal’s width decides whether they fit in one table or several:
    • C: the IP-Layer Capacity.
    • Loss Ratio and Lost: the Load PDUs lost, as a share of those sent and as a count. Reordered and Duplicates count the other Sequence Errors. Duplicates count as received.
    • DV: the one-way delay variation’s minimum, average and maximum.
    • RTTV: the round-trip time variation, lowest to highest, and its average, above the RTT minimum.
    • B: the IP-Layer Sender Bit Rate, what the Load Sender actually sent. In a Downstream Test the Client can only estimate the Server’s rate, and the footnote says so. In an Upstream Test it is measured on this end.
  • Maximum IP-Layer Capacity: the headline result, with its Sub-Interval’s Loss Ratio, RTT variation and Sender Bit Rate. Sub-Intervals 9 and 10 above were faster but lost more than 1%, so they don’t count, and the line ends with the Highest IP-Layer Capacity. With --first-mode-sub-intervals there is one such line per Capacity Mode.
  • Maximum Sub-Interval result received: when this Client learned the Maximum’s own Sub-Interval result, or unknown.
  • RTT minimum and Minimum one-way delay. The one-way figure compares the two ends’ clocks, so it means something only when they are synchronised.
  • Warnings, when there are any (below).
  • This host: whether the Don’t Fragment bit (DF) was set, the socket buffers the OS granted, and which Data Phase Engine ran with which fast paths. On Linux this names batched sending and receiving, UDP GSO and kernel timestamps. This example ran on Windows, which has none. Platform differences lists them.

In a terminal, headings are cyan, a clean result green, warnings and partial results yellow, and failures red. --color never keeps the output plain.

Was the network or a host the limit?

A search that rises past the path’s limit meets loss or growing delay there, so its Trial Intervals turn errored:

  • Below the 1 Gbps row the search climbs fast, and three Errored Intervals come back to back at its first overshoot. It confirms Congestion.
  • Above that row it climbs one row at a time. Through a shallow queue it cycles around the limit, with Errored Intervals often too few back to back to confirm Congestion. It met the limit when they recur among its last Trial Intervals.

The Client counts its own Trial Intervals. When no Test Connection’s search found the limit either way, it warns that the result may understate the path, and says why (below).

In a Fixed-Rate Test, which doesn’t search, look for yourself. If the Sender Bit Rate sits close to the Capacity in every Sub-Interval and the Loss Ratio stays near 0, the Load Sender may be the bottleneck rather than the path. A Server that falls behind logs BurstsSkipped. A Client that falls behind lists Skipped Bursts in its warnings.

What the warnings mean

A completed Test’s warnings appear under Warnings in the text results and in udpstp.Warnings in the JSON, each with a Code to match on rather than its wording (JSON results). The Test still exits 0. In the JSON, ErrorStatus carries the most severe one.

No Sub-Interval met the Performance Criterion. There is no valid Maximum IP-Layer Capacity (JSON ErrorStatus 49): the path lost more than --max-loss-ratio all along. A bottleneck with a shallow buffer does this. The search keeps stepping a row or two above its rate and back, losing a few percent there, as OB-UDPST’s does too. The Highest IP-Layer Capacity is then the path’s.

A Sub-Interval ran faster than the Maximum, or the last one much slower (JSON SubIntervalsAfterMaximumMissed). In a Search Test, every Sub-Interval after the Maximum’s lost too much to count, and either one of them ran more than 5% faster than the Maximum (--faster-sub-interval-margin), or the last one ran under 60% of it. Common causes:

  • A drop-tail bottleneck. After its first overshoot, the search cycles a row or two around the bottleneck’s rate, losing a few percent in each Sub-Interval. The Maximum then falls to a Sub-Interval of the climb, as low as half the path’s rate. This is most common from the 1 Gbps row up, where rows are 100 Mbps apart, and over long round trips.
  • A queue that keeps dropping after it overflowed, as CAKE can when a few Test Connections carry a fast link’s load. The rest of the Test then runs well below the link’s rate. With enough Test Connections that each carries under 1 Gbit/s, CAKE stops the loss within a few seconds and the Test reads the link’s rate.
  • A policer or shaper that let a burst through at first. The Maximum is then its sustained rate, which is right.
  • A path that slowed during the Test, or, Downstream, a Server that fell behind.

An Upstream Test in which this host left more than 1% of its Load PDUs unsent warns of its Skipped or Withheld Bursts instead.

Skipped Bursts: this host couldn’t send on schedule, so the result is limited by the sender. Withheld Bursts: the OS couldn’t take the datagrams fast enough.

The Server sent below its Fixed-Rate row. In a Downstream Fixed-Rate Test, the Server sent less than its row, as this Client estimated it. It may have skipped or withheld Bursts, so the Server limited the result. A Search Test’s Server doesn’t tell the Client its sending rate, so it gets the next warning instead.

The search never confirmed Congestion. No Test Connection’s search reached the path’s limit, as this Client counted their Trial Intervals, so the result may understate the path (JSON CongestionNeverConfirmed). The rest of the line names the cause (below).

Datagrams too large for the path with DF set. A hop’s MTU is below the Test’s IP packets, so the Test didn’t measure the path’s capacity. Rates up to 1 Gbps use IP packets of 1250 bytes, or 1500 with --traditional-mtu. Faster Jumbo rows use up to 9000. This is reported on Linux.

ICMP time-exceeded errors. The path is longer than MaxHops, so the load expired on the way.

Status PDUs never arrived, or the peer had missed Status PDUs. Status PDUs were lost. In a Downstream Test the Server lowers the rate when that happens.

Status PDUs failed authentication (mode 2). Status PDUs arrived that didn’t verify, so something on the path changed or injected them.

Nothing was received for 1 s, so this end stopped sending. The Watchdog ended the Test after 3 s of silence, without the STOP exchange. The results cover what was measured.

Load PDUs without a delay sample: the wall clock stepped. The clock was adjusted during the Test.

When the search never confirmed Congestion

In a Search Test, this Client classifies each Trial Interval itself, from the Status PDUs it sends in a Downstream Test and receives in an Upstream one, with the thresholds the Server accepted. A search found the path’s limit when it confirmed Congestion, or met the limit at its end: of its last 64 Trial Intervals, from the first Errored Interval among them on, at least 4 were Errored Intervals and at least 1 in 4, counting Hold Intervals of both kinds.

When no Test Connection’s search found the limit either way, the warning names the first cause that fits:

  • The maxBandwidth. The search stopped where a udpstp Server stops it, at the highest row at or below the maxBandwidth stated, or its share of it in a Multi-Connection Test. Above 1 Gbps the rows are 100 Mbps apart up to 7.2 Gbps and 720 Mbps apart beyond, so with --max-bandwidth 10000 the search stops at 9360 Mbps. An OB-UDPST Server keeps to the maxBandwidth only in a build with rate limiting, run with -B.
  • The top of the Sending Rate Table, judged from the Load PDUs sent at the peak. The Server’s MTU decides their size: the table tops out at 12 Gbps with the default 1500 bytes, and at up to 72 Gbps with 9000-byte Jumbo packets.
  • Delay variation, when more than half the Trial Intervals were Hold Intervals with a delay sample, between lowThresh and upperThresh. A Hold Interval in which no Load PDU arrived in order shows no delay variation and isn’t counted here.
  • Still rising, when the last Sub-Interval’s IP-Layer Sender Bit Rate is above every earlier one’s by more than Fast Mode adds with half its Trial Intervals clean, 100 Mbps at the defaults. A Load Sender held to a packet rate steps up once too, as its Load PDUs grow past the 1 Gbps row, so from about 500 Mbps a short Test can read as still rising where a longer one would name the Load Sender.
  • A Load Sender that stopped rising, the Server’s or this host’s, though few of the last 64 Trial Intervals were Errored Intervals.

Some paths and Servers make this warning more likely:

  • A managed queue can hold the search in Hold Intervals. fq_codel, with a Load Sender only just above the bottleneck, can hold a standing queue whose drops stay under seqErrThresh. The search’s last Trial Intervals are then Hold Intervals with no Errored Interval among them, until CoDel drops faster. A Test that ends before then names the Load Sender, or delay variation, though the path limited it.
  • The Client classifies by each Trial Interval’s largest delay variation, the delay range the RFCs judge, as a udpstp Server does by default. An OB-UDPST Server classifies by the average, which is lower, so against one, delay variation can be named for a search it didn’t hold.
  • This Client’s count can differ from the Server’s. It counts the one or two Trial Intervals it reports after the Server’s STOP1, and Status PDUs lost on the way back, which the Server never classifies. It can’t see the Server’s Lost Status Backoffs, which count as Errored Intervals there. Algorithm C’s Fast-Mode Retry restarts the Server’s run of Errored Intervals but not this Client’s. So the warning can come although the Server’s search confirmed Congestion.
  • An OB-UDPST Server counts more loosely. A Clean or Hold Interval doesn’t reset its count of Errored Intervals, where this Client, like a udpstp Server, needs them back to back (ADR 0005). Against an OB-UDPST Server, the warning can come although its search confirmed Congestion.

Multi-Connection results

A Multi-Connection Test prints the summed results first, then each Test Connection’s own Maximum IP-Layer Capacity. The summed table has an Active column: how many Test Connections received in that Sub-Interval. A Test Connection that didn’t complete says how it ended, and standard error says why, naming it.

A Downstream Test’s Test Connections are summed on one time grid. An Upstream Test’s are each measured by their own Load Receiver, at the Server, and summed by Sub-Interval number, so the sum is approximate, and a warning gives the largest offset between their Sub-Intervals.

The “This host” line covers all Test Connections: the smallest socket buffers the OS granted any of them, and the fast paths every one that completed used.

Security model

This document describes what udpstp protects, from whom, and how. It is written for Server administrators and Operators deciding whether to deploy udpstp, and for security reviewers. SECURITY.md says how to report a vulnerability.

Each defence below names the file, and the function, that implements it. The glossary defines terms such as Test Connection, Shared Key and Silent Rejection.

Summary

  • A Client selects authorized Server destinations locally before receiving responses. By default only its selected Server endpoint is accepted; authenticated Tests may use explicit per-Server alternate IP addresses through --allow-server-address SERVER=IP. Response authentication and Null Requests cannot add destination addresses. The sans-IO core enforces this before Activation and the Data Phase (crates/udpstp-core/src/client.rs, setup_source_authorized), retaining the selected Standard Port and IPv6 scope.
  • A Test starts only after the Client proves it holds a Shared Key that the Server knows. By default the Server answers nothing to a request that fails verification (Silent Rejection).
  • Only Clients authenticated with an Operator Key may run Fixed-Rate Tests.
  • A Test marks its traffic only with a DSCP the Server allows: 0 (best effort) unless the administrator adds more (allowed_dscp), so a Consumer can’t put a Test’s load into a priority class, and for Operator Keys any but network control, CS6 and CS7 (operator_allowed_dscp). A Client refuses a DSCP it didn’t ask for other than 0 (crates/udpstp-core/src/parameters.rs, clamp and unrunnable).
  • Keys come only from key files. Unix refuses unsafe ownership or group/other reading and writing. Windows refuses supported-policy mismatches and unverified permissions before reading keys, with distinct key file errors (Key handling). Keys never appear on the command line, in errors or in output, and they are wiped from memory when no longer needed.
  • UDPSTP authenticates. It does not encrypt. Anyone on the path can read Test parameters and results.
  • The Server enforces Admission limits (Test Connection caps, the Admitted Rate Cap, one Test per Source), drops replays inside the time window and budgets unverified Setup Requests globally and authentic ones per Source. A key holder can still sign requests from other addresses, spending those Sources’ budgets, so share keys only with Clients you trust. See Hardening.

Assets

AssetWhy it matters
Shared Keys and Operator KeysHolding a Shared Key lets anyone start Tests. Holding an Operator Key also allows Fixed-Rate Tests.
Derived KeysAnyone holding a Test Connection’s Derived Keys can forge its Control Phase PDUs, and its Status PDUs in mode 2.
The Server’s capacityLink bandwidth, Ephemeral Ports, sockets, CPU and Admission state. All of these are finite.
Third parties’ networksA Load Sender can put up to multi-gigabit load on a path. It must never be steered at a victim or across a shared path that nobody agreed to test.
The integrity of resultsA forged Status PDU can bias the Load Rate Adjustment Algorithm and therefore the reported Maximum IP-Layer Capacity. A forged Load PDU can add up to 1024 losses, which can keep Sub-Intervals from meeting the Performance Criterion; the Highest IP-Layer Capacity is reported next to it.
LogsLogs record Client addresses (personal data in many jurisdictions) and use disk space that an attacker could fill.

Actors and trust boundaries

  • Consumer: a Client user without an Operator Key: an ordinary Shared Key, or none where the Server allows Unauthenticated Mode. Trusted to run Tests, not trusted with Fixed-Rate Tests or unlimited resources.
  • Operator: holds an Operator Key, and is trusted with Fixed-Rate Tests and longer Tests.
  • Server administrator: controls the Server host, its key file and its configuration. Fully trusted.
  • Off-path attacker: can send datagrams with any source address but cannot see the traffic between Client and Server.
  • On-path attacker: can observe, drop, delay, modify, replay and inject datagrams between Client and Server.
  • Local user: an unprivileged account on the Client or Server host.
The trust boundaries. On the Client host the Client reads its private key file and writes its output. On the Server host the Server reads its key file and configuration and writes its logs. Local users have no access to the key files. Between the hosts, the untrusted network holds an on-path attacker, who can observe, modify and replay the Control Phase, which is authenticated, and the Data Phase, whose Status PDUs are authenticated in mode 2 only; an off-path attacker, who sends forged or replayed PDUs from spoofed sources; and third-party victims, at whom the Server must never reflect load.

The boundaries are the network (everything arriving on a socket is untrusted until verified), the key files (readable only by the account running udpstp, which udpstp checks on every platform, see Key handling), and the logs, which can contain attacker-controlled data such as source addresses.

Assumptions: the hosts themselves are not compromised, Shared Keys are distributed over a secure channel outside UDPSTP, and both ends’ wall clocks are within 5 seconds of true time.

Threats and defences

Load PDU timestamp gaps are bounded. A Load Receiver’s sender estimate closes at most the negotiated Test Interval’s number of st plus one per Load PDU, then skips any remaining empty intervals in one step (crates/udpstp-core/src/data_phase.rs, SentEstimate::add). A Consumer can negotiate at most 60 s with st of at least 5 ms, so at most 12,001 closures are needed, independent of the wire timestamp gap. Operator Keys may permit longer Test Intervals and correspondingly larger reporting limits. This bounds the core callback’s interval processing without changing the Engine Thread model (ADR 0008) or send catch-up budget (ADR 0012); it is not a measured latency guarantee.

RFC references are to RFC 9946 unless stated otherwise. Code paths are relative to the repository root.

ThreatAttackerDefenceRFC basisImplemented in
Forged Setup RequestOff-pathMode 1: the Client signs the request with a Client Authentication Key derived from the Shared Key and the request’s own authUnixTime. The Server verifies the HMAC-SHA-256 over the bytes as received, in constant time, and answers a failure with Silent Rejection.§5.3.1, §5.4.1, §6.2; ADR 0001, 0009crates/udpstp-core/src/server.rs (authenticate_setup), crates/udpstp-wire/src/auth.rs (verify_received)
Forged Setup or Activation ResponseOff-path, on-pathThe Client verifies each response with the Server Authentication Key and the time window. It accepts responses only from the Server’s address or the Ephemeral Port, and ignores a response that fails verification rather than ending the Test.§5.3.1, §6.3, §7.3; ADR 0009crates/udpstp-core/src/client.rs (response_ignore_reason, handle_datagram)
Forged Activation RequestOff-pathThe request must carry the Setup’s authMode and keyId and verify with that Test Connection’s Client Authentication Key. Each Test Connection is activated at most once.§6.2, §7.2; ADR 0009crates/udpstp-core/src/server.rs (verify_activation, handle_activation_request)
Replay of a Setup or Activation Request after the time windowOn-pathauthUnixTime must be within ±5 s of the receiver’s clock.§5.3.1; ADR 0009crates/udpstp-core/src/keys.rs (auth_time_acceptable)
Replay within the time window, including onto sibling Test ConnectionsOn-pathA replay cache keyed on keyId and authDigest, shared by the Standard Port and every Ephemeral Port, remembering each verified request for 11 s once it is within its Source’s budget, so a key holder’s flood can’t fill it (its size follows the global budget); Activation Requests are keyed on their sender too, since a Multi-Connection Test’s can be identical, and must come from the Client’s IP address. A copy spoofed from another port of the Client’s address can still activate a sibling first. Unauthenticated Mode has no replay protection.§5.3.1crates/udpstp-core/src/replay.rs, server.rs
Reflection: a spoofed source makes the Server send to a victimOff-pathRequests that fail verification get no reply. A verified Setup Request yields a Setup Response and a Null Request, as the RFC requires: 104 bytes for 56, which the per-Source budget limits. Once a Source’s budget runs out, the Server answers only its first refusal. Load PDUs are sent only after a verified Activation Request from the Client’s IP address, and a Load Sender stops within 1 s when Status PDUs stop arriving.§5.1 items 8-9, §5.3.1, §11 item 2crates/udpstp-core/src/server.rs, replay.rs, budget.rs, data_phase.rs (RECEIVE_TIMEOUT)
Amplification using multicast or broadcast addressesOff-pathSetup Requests and Responses from or to a multicast address, or 255.255.255.255, are discarded silently, IPv4-mapped forms included, and so are Activation Requests from one. The Client refuses such a Server address. A subnet’s directed broadcast address can’t be recognised without its prefix length. The Server checks where each datagram arrived, and so does the Client on Linux and macOS; a Windows Client checks sources only (ADR 0007).§6crates/udpstp-core/src/io.rs (is_multicast_or_broadcast), server.rs, client.rs
Flood of unverified Setup Requests (CPU)Off-pathA request with an unknown keyId, or a key outside its accept lifetime, is discarded before any cryptography, through an indexed lookup. A global budget (unverified_setups_per_second, 20000 by default) limits the requests that reach a KDF run and an HMAC; the rest are dropped silently. Derived Keys are cached, so a flood sharing them costs one HMAC per PDU. See Admission under load.§11 item 4crates/udpstp-core/src/budget.rs, server.rs, keys.rs (KeyTable::accepting)
Exhaustion of Ephemeral Ports, sockets and bandwidthKey holder, replayerA global and a per-Test cap on Test Connections, the Admitted Rate Cap (CAPEXC), one Test per Source by default, per-Source and per-key budgets charged after the HMAC, a separate share for Unactivated Test Connections with eviction by key, a pre-Activation Watchdog, optional Quotas, and Admission released on every exit path, an engine panic included. See Admission under load.§4.2, §11 items 4-6crates/udpstp-core/src/server.rs (admission_refusal, eviction_candidate, handle_socket_closed, handle_timeout), budget.rs, quota.rs, data_phase.rs; crates/udpstp/tests/faults.rs
Hijacking a Test ConnectionOff-pathIn the Data Phase, the Data Phase Engine accepts datagrams only from the Test Connection’s peer address and port. Before Activation, the Ephemeral Port accepts only the Client’s IP address, and then locks to the address of the first authenticated Activation Request.—crates/udpstp-runtime/src/socket/mod.rs (Socket::receive)
Forged Status PDUs steering the rateOn-pathMode 2 authenticates Status PDUs. Stale spduSeqNo values are excluded from rate decisions and can’t deliver STOP. A Client executing a Server’s Sending Rate Structure zeroes fields beyond fixed sanity limits. The Server stops an Upstream Client that sends over 1.5 times the highest rate it assigned in the last second for 3 Trial Intervals.§5.3.2, §8.1, §8.2crates/udpstp-core/src/data_phase.rs (handle_pdu, status_authentic, sanitize, police)
Forged Load PDUs hiding lossOff-path with the Test Connection’s addresses, on-pathLoad PDUs are never authenticated. A jump of more than 1024 in lpduSeqNo (a first Load PDU more than 1024 from 1) counts only once the next Load PDU continues from it, otherwise it is ignored; and 32 out-of-order Load PDUs received in a consecutive run more than 1024 behind bring counting back to them. So one forged Load PDU can move counting at most 1024 ahead, and two forged in a row hide loss until 32 genuine Load PDUs arrive in a row, which heavy loss can delay. Forged Load PDUs can still add loss (up to 2^31 for a confirmed jump), and forged out-of-order ones can cancel loss one for one. Only Load PDU authentication would stop that, and RFC 9946 has none.§8.2crates/udpstp-core/src/data_phase.rs (Lookback)
Forged STOP set or clearedOn-pathClearing STOP cannot extend a Test: each end stops on its own Test Interval timer. Setting STOP ends the Test early, which the RFC accepts as an annoyance only. In mode 2 a forged Status PDU is ignored, and a replayed one is stale, so neither delivers STOP; but in a Downstream Test the Server’s STOP travels in Load PDUs, which no mode authenticates, so a forged one still ends the Test.§11crates/udpstp-core/src/data_phase.rs, proved by crates/udpstp-core/tests/stop_exchange.rs and mode2_status.rs
Downgrade from mode 2 to mode 1, or to Unauthenticated ModeOn-pathauthMode is covered by the digest. A mode 1 Client ignores responses carrying another authMode. The Server refuses authMode 0 unless configured to accept it. An Activation Request must repeat its Setup’s authMode. The Client defaults to mode 2 and never falls back to mode 1 on its own.§5.3, §6.2crates/udpstp-core/src/client.rs (response_ignore_reason), crates/udpstp-core/src/server.rs (handle_setup_request, verify_activation)
Consumers running Fixed-Rate TestsConsumerA Fixed-Rate request authenticated with anything other than an Operator Key is rejected with BADPARAM, and no load is sent.§4.1crates/udpstp-core/src/server.rs (is_fixed_rate), proved by crates/udpstp-core/tests/fixed_rate.rs
Consumers running long or fast TestsConsumerThe Test Interval is capped at 60 s, or at an Operator Key’s own maximum, and slowAdjThresh × trialInt at 500 ms except for Operator Keys. A search without an Operator Key starts no higher than max_start_row (row 100 by default: from the gigabit row Algorithm B would drain one row a Trial Interval, flooding a slow link for the whole Test), and the algorithm never exceeds the Client’s maxBandwidth.§4.2, §5.1 items 4-5, §7.2.1, §10, §11 items 5-6crates/udpstp-core/src/parameters.rs (clamp, Allowance), server.rs (max_start_row)
Key leak through the command lineLocal userThere is no option that takes a key. Keys come only from key files.—crates/udpstp/src/main.rs, proved by crates/udpstp/tests/key_files.rs
Key leak through file permissionsLocal userUnix refuses unsafe ownership and group/other content access. Windows assesses the same opened file against a bounded private-file policy and refuses mismatches or unverified permissions before reading content.ADR 0015crates/udpstp/src/keyfile.rs, crates/udpstp-key-permissions (Key handling)
Key leak through memoryCrash dump, swapShared Keys, Derived Keys, HMAC state and the key file’s bytes are zeroed when freed.—crates/udpstp-wire/src/auth.rs (SharedKey, DerivedKeys), crates/udpstp/src/keyfile.rs (load)
Key leak through logs and errorsLog readersKey types print as <redacted>. Key file errors give the file, the line and the fault, never the value.—crates/udpstp-wire/src/auth.rs, crates/udpstp/src/keyfile.rs (toml_error_message), proved by crates/udpstp/tests/key_files.rs
Clock skewMisconfigurationA ±5 s window, both edges inclusive, checked on requests and responses and computed modulo 2^32 so that it survives the 2106 wraparound. There is no extra grace period.§5.3.1; ADR 0009, 0010crates/udpstp-core/src/keys.rs (auth_time_acceptable)
Memory-unsafe parsingAnyoneProtocol decoders and the Server core forbid unsafe and are fuzzed. Windows permission decoding and policy also forbid it; only the bounded native inspection adapter permits it (ADR 0015).—Workspace forbid lint; udpstp-key-permissions denies it except src/native.rs; decoder bounds use safe slices.
Log floodingOff-pathNothing is logged per PDU. Notices an attacker can trigger (dropped or refused requests, socket errors, invalid PDUs and their like) are logged ten of a kind every 10 s at most; the rest are counted and the count logged when the window ends.—crates/udpstp/src/log.rs (Limiter)

Admission under load

The Server bounds what a flood can cost it, and shares its capacity between keys. The settings below are in Server configuration.

Forged Setup Requests. A request with an unknown keyId costs almost nothing to drop. One with a known keyId spends the global budget for unverified Setup Requests, unverified_setups_per_second, before its KDF run and HMAC. At the default of 20000 a second, this checking takes at most about 6% of one core on a 2017 laptop CPU, or 14% with the CPU held at its base clock. Beyond the budget, requests are dropped silently, and a forged flood crowds out Clients’ Setup Requests until it stops. Lower the budget on a slower host.

A key holder’s flood. Authentic Setup Requests reach Admission, which costs far more. Each Key Table row has its own budget, key_setup_burst and key_setups_per_second (1000 a second by default), and Unauthenticated Mode counts as one row. Requests beyond it are dropped after their HMAC, so one key can’t make the Server fall behind, though floods on many keys together still can.

Unactivated Test Connections have a share of their own, max_unactivated_test_connections, by default half of max_test_connections and at least test_connections_per_test. When the share is full, the Server closes the oldest Unactivated Test Connection of the key holding the most. So a flood on one key evicts its own, and other keys’ Clients keep their place until they activate. This is a fair share, not a guarantee:

  • A key’s flood keeps out that key’s own Clients whose round trip is longer than the time the flood takes to fill the share. At the key’s default budget, that is Clients about 128 ms or more away.
  • A flood split over several keys takes a share from every key that holds more than its part.
  • Raising the share, for example to 256, needs more keys to displace another key’s Test, at the cost of more sockets and memory, and at least 512 Ephemeral Ports with the default cap.

To spot a flood, watch key_id on TestConnectionAdmitted and unactivated_test_connections_evicted in Statistics. Remove or expire a key that floods, then restart the Server.

Places for activated Tests. A Test’s first activation reserves places in max_test_connections for all its Test Connections. If they no longer fit, the whole Test is refused, so no Test starts with only part of its Test Connections. Activated Tests can fill the cap, but a Setup flood that never activates can’t.

The Admitted Rate Cap counts the maxBandwidth each Test states, in each direction, from its first activation to its end, whatever it sends. Set require_max_bandwidth with it, so that every Test counts. One Test without an Operator Key may state at most max_test_share of the cap, half by default.

Key handling

Formats. A key file whose name ends in .toml is a TOML Key Table. Any other file is read as an OB-UDPST udpst.keys file (crates/udpstp/src/keyfile.rs).

  • TOML Key Table: one [[key]] row per Shared Key, with RFC 7210’s fields.
    • admin_key_name (unique) and key_id (the keyId, 0-255).
    • Optional kdf and alg_id. Both must be HMAC-SHA-256 if present.
    • Exactly one of key_text (used as its UTF-8 bytes) or key_hex.
    • Optional send and accept lifetimes in YYYYMMDDHHMMSSZ form.
    • Optional operator and max_test_interval, an extension to RFC 7210 (ADR 0010).
    • Unknown fields, empty keys and malformed values are errors.
  • OB-UDPST files: <keyId>,<key> lines, with keys of up to 64 bytes. These files have no lifetimes, so their rows are always valid.

The Shared Key is used as raw bytes and never cut at a zero byte (ADR 0001). udpstp sets no minimum key length. Use at least 32 random bytes, for example key_hex from openssl rand -hex 32.

Permission check. On Unix, the file must be a regular file with no group or other permission bits for reading or writing (mode 0600 or stricter), owned by the user running udpstp or by root. On every platform it must be at most 1 MiB. Otherwise udpstp exits with a key file error. Running udpstp setuid is not supported. The directory holding the file isn’t checked: keep it writable only by its owner.

Windows applies ADR 0015 to every Shared Key file loaded by either role, in either format (Conformance Record key-file-private). It trusts only the running process account, SYSTEM and built-in Administrators, comparing binary SIDs without group expansion. Files that meet the supported local-NTFS policy proceed quietly. Dangerous grants to outsiders, untrusted ownership and absent/null DACLs are policy mismatches; unsupported storage, rights, flags or ACE types, malformed descriptors and inspection failures are unverified. Unsupported data takes precedence over a mismatch. Both are refused before content is read, with exit status 51 and distinct diagnostics naming the file, reason and remediation, without claiming effective exposure. Client errors use stderr with no JSON result; Server errors reach stderr and any configured startup log as StartupError. Read and parse errors retain their existing handling. Ordinary --print-config validates keys too. Its --settings-only modifier previews settings without opening keys, logs or listeners and establishes no key readiness.

Native inspection and content reading use the same handle with read-only sharing. The adapter owns its initialized, aligned descriptor buffers and temporary token handle, bounds descriptor size to 128 KiB and ACE count to 1,024, and returns owned data. Only udpstp-key-permissions/src/native.rs permits own unsafe; descriptor decoding, application policy, protocol core and network runtime forbid it. Ordinary content writers and replacement opens conflict with the reader, but ACL changes remain possible. The assessment does not determine universal effective access, freeze permissions, protect writable containing storage, handle impersonation or cover remote/non-NTFS storage. Runtime PowerShell and unmaintained ACL wrappers remain rejected.

Use the manual reconstruction for a dedicated profile folder, or the service installer for unattended hosting. The procedure removes pre-existing explicit grants and examines the actual file, not only its parent. Copy files into protected storage rather than moving them with their old permissions, then reconstruct their ACLs. Never keep them under a folder created directly under C:\, where ordinary accounts can modify the containing storage.

Zeroization. SharedKey and DerivedKeys wipe their bytes when freed, and so do the KDF’s intermediate blocks and the buffer the key file is read into (zeroize, and the hmac crate’s zeroize feature). That buffer is sized once and never reallocated, and a file that grows while it is read is refused. Limits:

  • Copies made inside the TOML parser are beyond udpstp’s reach.
  • Keys are not locked in memory, so they can reach swap.
  • A core dump taken while the process runs can contain the Key Table.

Disable core dumps for the service.

Operator Keys. A Server Key Table row marked operator = true is an Operator Key. Privileges come from the row that verified the Setup Request, not from anything the Client claims. An Operator Key may carry its own max_test_interval for Tests longer than 60 s. Give Operator Keys their own keyIds, and never share them with Consumers.

Lifetimes and rollover (ADR 0010, crates/udpstp-core/src/keys.rs):

  • The Server accepts a keyId only through a row whose accept lifetime includes the Server’s clock. The Client sends with the valid row that has the newest send lifetime start. If several rows are equally new, the Client refuses to guess, and --key-id must pick one.
  • Two rows may share a keyId only if neither their send nor their accept lifetimes overlap. The Key Table is refused at start-up otherwise.
  • A Test Connection keeps its Derived Keys when its row expires mid-Test.
  • For a smooth rotation, give the new key a new keyId whose lifetimes overlap the old one’s, then retire the old keyId.
  • The key file is read once at start-up. Adding rows in advance, with future lifetimes, avoids restarts.

Authentication Modes and Unauthenticated Mode

Mode 1 authenticates the Control Phase: the Setup and Activation exchanges in both directions, and the Null Request.

  • It proves that the peer holds the Shared Key, protects every Control Phase field from modification, and rejects replays older than ±5 s.
  • It does not authenticate Load PDUs or Status PDUs, so an on-path attacker can forge Data Phase traffic.
  • It does not cover IP addresses or ports.
  • It detects a replay inside the window only on the Server that saw the request, with its replay cache.
  • All Consumers who share one Shared Key look the same to the Server.

Mode 2 adds authentication of every Status PDU, in both Test directions. It protects the rate decisions and the STOP signal carried in Status PDUs. Load PDUs are never authenticated, by design of the RFC. The Status PDU’s sender signs it with its own Derived Key, the Client’s in a Downstream Test and the Server’s in an Upstream one; the receiver checks authMode, keyId, the digest and the time window, and ignores one that fails, so its Watchdog ends the Test if they keep failing (crates/udpstp-core/src/data_phase.rs). The Client uses mode 2 by default and never falls back to mode 1 on its own, so an attacker can’t downgrade a Test; mode 1 is chosen with --auth-mode 1, or for one Server with --mode-1-server. A Server that rejects mode 2 with an AUTHINV it can’t sign, as OB-UDPST does, is reported as probably not supporting it (Event::AuthMode2Unsupported). Two limits remain. A replayed Status PDU, stale but authentic, still counts as heard, so an on-path attacker who drops the genuine ones can keep the Load Sender sending blind for a few seconds more before its Watchdog ends the Test. And a Status PDU names no Test Connection, so two Test Connections whose Derived Keys match (the same key and the same Setup second, as in a Multi-Connection Test) accept each other’s Status PDUs: an on-path attacker can move one’s into the other. Both follow from RFC 9946’s design.

Unauthenticated Mode is OB-UDPST’s authMode 0. RFC 9946 does not define it: IANA lists the value as “Not used”. udpstp offers it only so that it can interoperate with default OB-UDPST installations, and only when both ends opt in: udpstp client --unauthenticated, and allow_unauthenticated = true (or --allow-unauthenticated) on the Server. A Server that allows it still accepts mode 1. Unauthenticated Mode protects nothing. Anyone who can send a datagram to the Server can start a Test, parameters and results can be forged, and there is no replay protection. Its requests spend no per-Source budget, and every coded rejection goes to the claimed source address, so a spoofed request makes the Server answer a victim, bounded only by the global budget for unverified Setup Requests. Beyond that and the Test Connection caps, Unauthenticated Mode has no defence. Enable it only on networks where every host is trusted.

Silent Rejection and Troubleshooting Mode. By default, authentication failures get no reply, so a prober learns nothing. Troubleshooting Mode (troubleshooting = true, or --troubleshooting) answers them with an unsigned Setup Response carrying AUTHREQ, AUTHNC, AUTHINV, AUTHFAIL or AUTHTIME, for debugging. AUTHTIME, whose digest is valid, is signed. It is off by default, and while it is on the Server warns at startup and every minute (Event::TroubleshootingModeOn). It answers at most 10 failures a second. An unknown keyId and a wrong digest both get AUTHFAIL, but anyone can learn whether keys are configured (AUTHNC) and unauthenticated Tests allowed (AUTHREQ), and a spoofed request gets its reply, so turn it off after debugging. Activation Requests that fail verification stay silent even then: a Test Connection whose Setup was authentic rarely fails there.

Out of scope

  • Confidentiality. UDPSTP does not encrypt. An observer on the path sees addresses, keyIds, Test parameters, Status PDU contents and so the results. Use a VPN or a private network if this matters.
  • Key distribution and identity. How Shared Keys reach Clients is outside RFC 9946 (§5.4), and outside udpstp. A keyId identifies a key, not a person.
  • An on-path attacker who drops or delays traffic. Such an attacker can always prevent a Test or lower its result. A capacity measurement reports what the path delivers, including the attacker’s interference.
  • Volumetric floods at the Server’s link. Traffic that saturates the link before it reaches the host needs upstream filtering.
  • Compromised hosts. An attacker with root, or with the service account, can read keys and change results.
  • A key’s holders against each other. All Clients sharing a Shared Key look the same to the Server, and so does everyone where Unauthenticated Mode is on. A key holder’s flood can keep that key’s own distant Clients from starting Tests, and a flood split over several keys takes a share from every key. No defence finer than the key is possible. Admission under load explains the limits.
  • Consent of the measured path. RFC 9946 §11 requires cooperating hosts. udpstp enforces this through key possession, and Server administrators decide who receives keys. Quotas of Tests and load bytes are optional.

Hardening for Server administrators

  1. Keep authentication on. Leave allow_unauthenticated off (the default) on any Server that is reachable from networks you don’t control.
  2. Limit who can reach the Server. Allow-list Client networks in your firewall where you can. Give each population of Clients its own key, and keys only to Clients you trust. Admission under load explains what the Server’s own limits hold back.
  3. Protect the key file. On Linux and macOS, keep it owned by the service account with mode 0600. On Windows, udpstp refuses a file that other accounts can access; set it up as udpstp on Windows shows. Use random keys of at least 32 bytes, a separate key per population of Clients, and separate Operator Keys. Use lifetimes to plan rotations.
  4. Open only what is needed. A stateful firewall needs only the Standard Port (24601) open inbound: the Null Request the Server sends from each new Ephemeral Port lets that Test Connection’s traffic back in (tested with nftables). A stateless filter, a NAT and Windows Defender Firewall need the Ephemeral Port range too, 49152-65535 by default. Narrow it with ephemeral_ports so that the rule stays small. Open the firewall has the details.
  5. Run unprivileged. The Standard Port is above 1024, so the Server needs no root privileges or capabilities. Run it under a dedicated account with core dumps disabled (for example ulimit -c 0).
  6. Synchronise clocks. Run NTP or similar. Clocks more than 5 s apart make every authenticated Test fail.
  7. Handle the logs. The Server logs its events to standard output, human-readable or as JSON (log_format), including discarded requests and full Client addresses; startup warnings go to standard error. Events an attacker can cause are limited to ten of a kind and a hundred in all every 10 s, the rest counted. Every statistics_interval (60 s by default) a Statistics event gives the counters since start: Tests and Test Connections admitted, those active, refusals and drops by reason, authentication failures, rate-limited Setup Requests and stale Status PDUs; each Test ends with a TestSummary of its Client address, keyId, direction, parameters, bytes and result, limited like the events above when none of its Test Connections was activated. Send the output to a log system that rotates it, and treat it as personal data. address_privacy shows Client addresses in full (the default, for abuse handling), pseudonymized (anon- and 8 bytes of HMAC-SHA-256 over the IP address, keyed with the hex key in address_privacy_key_file, so one Client keeps one pseudonym while the key is kept) or truncated to the IPv4 /24 or IPv6 /48.
  8. Use the hardened unit or the container image. contrib/systemd/udpstp-server.service runs the Server as a user systemd creates when it starts, with no capabilities, a read-only view of the system, UDP and the journal’s socket only, a system call filter and core dumps off, and passes it the Key Table as a credential; scripts/systemd-check.sh checks it with systemd-analyze verify and security. contrib/container/Containerfile builds an image with no shell that runs the Server as uid 65532. Run it as a service has the details.

Verify a release’s build provenance

Release checksums detect a download that differs from the published hash. Build attestations add a separate, signed check of where it was built. The install scripts don’t perform this check.

Use the GitHub CLI 2.97.0 or later, signed in with gh auth login. Download the archive yourself, since the install scripts delete theirs. For Linux on x86_64:

tag=v0.1.0
archive=udpstp-$tag-x86_64-unknown-linux-gnu.tar.gz
gh release download "$tag" --repo pederbe/udpstp --pattern "$archive"
gh attestation verify "$archive" --repo pederbe/udpstp \
  --signer-workflow pederbe/udpstp/.github/workflows/release.yml \
  --source-ref "refs/tags/$tag"

For Windows, in PowerShell:

$tag = 'v0.1.0'
$archive = "udpstp-$tag-x86_64-pc-windows-msvc.zip"
gh release download $tag --repo pederbe/udpstp --pattern $archive
gh attestation verify $archive --repo pederbe/udpstp --signer-workflow pederbe/udpstp/.github/workflows/release.yml --source-ref "refs/tags/$tag"

Use your release’s tag and archive name, and continue only if verification succeeds. This checks the signed provenance against the repository, the Release workflow and the source tag. It doesn’t prove the code is safe or stop a tag from moving. GitHub’s attestation guide explains the guarantees.

Stable interfaces

What you can build on from udpstp 0.1.0, and how it changes. The command line, the Server’s settings and its JSON log settle during 0.x and become stable at 1.0. The decision and its reasons are ADR 0013; every change is in the changelog.

What is stable

InterfaceWhat is stableReference
Exit statusesThe rule behind them, and every value assignedbelow
JSON results (--format json)Key names, types, enumerated values. The OB-UDPST part keeps OB-UDPST v9.1.0’s schema, except the recorded differencesJSON results
Key filesThe TOML Key Table’s columns and format; OB-UDPST’s udpst.keys as OB-UDPST reads it; the pseudonym key filebelow
stdout and stderrWhich output goes wherebelow
Address pseudonymsThe algorithm, so one key gives one Client the same pseudonym in every versionbelow

Stable from 1.0

InterfaceWhat becomes stableReference
Command line and the Server’s TOML settingsOption and key names, value spellings, defaultsudpstp client --help, udpstp server --help, udpstp server --print-config; Server configuration
The Server’s JSON log (log_format = "json")Event names, field keys, enumerated valuesServer log

Until then, a 0.x minor release may rename or remove one of these, with no deprecation period. The changelog lists each change under Changed or Removed, so read it before upgrading a script or a configuration file.

What is not

Human output adapts to the width of the terminal it is written to. Redirected output and a terminal whose width is unknown use 80 columns. Help, diagnostics and progress wrap with continuation indentation; results use a full table when it fits and grouped tables otherwise, with repeated Sub-Interval numbers and a nearby legend. An identifier wider than the terminal stays intact on a continuation line so it remains copyable. The human Server log marks continuation lines with |; each event still starts with its own timestamp and level. JSON and configuration output are unaffected by this layout.

  • Human-readable text: the text results, progress lines, the wording of warnings and errors, --help, and the human log’s messages and layout. Parse the JSON results and the JSON log instead.
  • Free text inside the JSON: ErrorMessage, ErrorMessage2, a warning’s Message, the log’s message and error fields. Use ErrorStatus, a warning’s Code, the log’s event and reason.
  • The format of udpstp --version.
  • UDPSTP_FAULTS and UDPSTP_BLESS, which exist for udpstp’s own tests.
  • UDPSTP_KERNEL_TX_TIMESTAMPS, an experimental setting for precision tests: set to 1 on Linux, a Load Sender asks the kernel to timestamp what it sends, and the Client’s JSON results add udpstp.KernelSendTimestamps (Samples, LagP50Us, LagP99Us, LagMaxUs: how much later than lpduTime the kernel sent them). Set to kernel-count, it asks for the same timestamps matched by the kernel’s own count of the messages on every kernel, as before Linux 6.13, rather than by numbers the messages carry, so a result can be checked on that send path. The variable, its values and the block may change or go in any release.
  • UDPSTP_ENGINE_CATCH_UP_US, an experimental setting for measurements: the Data Phase Engine’s catch-up budget in µs (ADR 0012), in place of 1 ms (2 ms on Windows). It may change or go in any release.
  • UDPSTP_ENGINE_THREADS and UDPSTP_ENGINE_GROUPING_US, experimental settings for measurements of a Linux Server, whose Data Phase Engine runs on a pool of Engine Threads: the most Engine Threads the pool starts, in place of the CPUs the process may use; and the pool’s wake-up grouping window in µs, 0 (none) by default, since no measurement has yet shown a window worth making the default (ADR 0008). They may change or go in any release.
  • UDPSTP_ENGINE_HOLD_OFF_US, an experimental setting for measurements: on Linux it sets the Data Phase Engine’s receive hold-off in µs, 0 for none, in place of the one derived from the receive buffer, and a Load Receiver takes it after every turn that read datagrams (a Load Sender, when flooded), where it takes the derived one only when more than one datagram is due within it. It may change or go in any release.
  • The library crates (udpstp-wire, udpstp-core, udpstp-runtime, udpstp-key-permissions): they follow Cargo’s semver rules only, and aren’t a supported interface in 0.x.

Terminal color

--color auto|always|never is global: it can precede or follow client or server. The default, auto, checks stdout and stderr independently and styles only a terminal that supports color. A nonempty NO_COLOR or TERM=dumb disables automatic styling; an empty NO_COLOR does not. always and never override these environment defaults. CLICOLOR, CLICOLOR_FORCE and CI do not override this policy. never removes bold, dimming, colors and decorative status symbols. Redirected automatic output keeps the plain presentation.

The same policy covers help, parser errors, startup diagnostics, Client progress and text results, and human Server console logs. always keeps styling when human output is redirected. JSON results, JSON logs, printed TOML configuration and daily log files stay plain in every mode, including Windows service files. Startup diagnostics copied to a log file stay plain there while their stderr copies follow stderr’s policy.

InformationStyle
Errors and failed or interrupted outcomesBold red, with ✗ on the Test outcome
Warnings and qualified resultsBold amber, with ⚠ on the outcome and warnings heading
Completed Test without warningsBold green, with ✔ on the outcome
Section and help headingsBold purple
Progress and INFO log labelsBlue, with ▶ on the start of a step
Table headings, timestamps and host metadataMuted gray
Commands and option namesBold, default foreground
Final Maximum IP-Layer CapacityThe Test outcome’s color: green for clean completion, amber for qualifications, red for failure or interruption
Other measurements, addresses and ordinary valuesDefault foreground

Colors use RGB foregrounds on the terminal’s default background: purple #bd93f9, blue #5cb8ff, green #5af78e, amber #f39c12, red #ff5c57 and gray #70788c. Terminals with fewer colors may approximate them. Client results show one aligned Sub-Interval table when it fits, or two metric groups with repeated Sub-Interval numbers. Very narrow groups become labelled values. Final metrics show each Capacity Mode’s maximum once, followed by supporting measurements, warnings and the aggregate Test outcome. Labels remain understandable without color; a high rate alone is no reason for green. Symbols supplement the outcome text. Styling preserves copyable values and table alignment, with wrapping adapted to the displayed symbol width.

How a stable interface changes

These rules cover the interfaces stable from 0.1.0, and from 1.0 the command line, settings and JSON log too.

  • Additions come in a minor release: new options, settings, JSON keys, log events and fields, enumerated values, exit statuses and Key Table columns. Your tools should ignore what they don’t know: an unknown key, field, event or value, and treat an unknown non-zero exit status as a failure.
  • A renamed or removed input (an option, a setting, a value, a Key Table column) is still accepted for one minor release, with a warning on stderr naming its replacement. A name deprecated in 0.3.0 works through 0.3.x and may go in 0.4.0.
  • A renamed or removed output (a log event, field or value, a JSON key or value) is written under both names for one minor release, then only under the new one.
  • A default changes in a minor release, announced in the changelog of the minor release before. A security fix may change one at once; the changelog says so.

Each is in the changelog, under Added, Deprecated, Changed or Removed. Read it before upgrading across a minor release.

Exit statuses

A Client’s exit status is its ErrorStatus, the one OB-UDPST reports, except that a Test that completed with only warnings exits 0. udpstp’s own statuses use values OB-UDPST leaves unused. New statuses may be added; an assigned one never changes meaning.

StatusMeaning
0The Test completed. With --format json, ErrorStatus gives its most severe warning: 49 no valid Maximum IP-Layer Capacity, 4, 5, 3, 2 or 6 as JSON results lists
50The configuration or command line is invalid
51The key file can’t be read or is invalid
75The Client or Server couldn’t start
100 + codeThe Server refused the Setup Request with that Command Response (102 to 113); 100 for a code RFC 9946 doesn’t define
150 + codeThe Server refused the Activation Request; 152 is BADPARAM, 150 an undefined code
107, 108, 157, 158A Setup or Activation Response failed verification, or its time was outside the window
200The Test didn’t complete
201The Server coerced a parameter the Client was told to refuse
202The Server set a parameter the Client can’t run with
203A Multi-Connection Test’s Servers gave different parameters
204The Test was interrupted, or a write to stdout failed. On Linux and macOS an interrupted Client ends by the signal (130 for Ctrl-C), and 204 is the JSON’s ErrorStatus
255Any other error

Running a Test says what to do about each.

stdout and stderr

Programstdoutstderr
udpstp client --format jsonThe JSON results, and nothing elseProgress, warnings, errors
udpstp client (text)Progress and the text resultsErrors, and warnings about this host
udpstp serverThe logErrors and warnings at startup
udpstp server with log_fileNothing; the log goes to daily files named from log_file, with the startup’s errors and warnings as StartupError and StartupWarningErrors and warnings at startup
udpstp server --windows-serviceNone: a service has none; its log, startup included, goes to log_fileNone
udpstp server --print-configThe effective configuration, as TOMLErrors
udpstp server --print-config --settings-onlyValidated settings as TOML, without opening keys, logs or listenersSettings errors

Windows Shared Key files must meet ADR 0015’s supported local-NTFS policy. Mismatches and unverified permissions are distinct key file errors (51), refused before content reading, with actionable reasons that do not claim effective exposure. Client errors leave stdout empty; Server errors also reach a configured daily log as StartupError. Ordinary --print-config validates keys. Its --settings-only modifier previews settings and establishes no key readiness; it requires --print-config and conflicts with --windows-service. See Key handling.

Key files

A TOML Key Table that is valid today keeps its meaning in every later version: columns are added, never removed or read differently. It may start with format = 1, which a file without it is too.

The reverse doesn’t hold: a file with a column this version doesn’t know, or a higher format, is refused rather than read in part, since the column it would skip may be a restriction such as a lifetime or a Quota. Upgrade every Server and Client that reads a Key Table before you add a new column to it. Key files lists the columns.

OB-UDPST udpst.keys files are read as OB-UDPST reads them. The pseudonym key file is hexadecimal digits, 16 bytes or more, with whitespace around them and nothing else.

Address pseudonyms

With address_privacy = "pseudonymize" (the Server) or --address-privacy pseudonymize (the Client), an address shows as anon- and the lowercase hexadecimal of the first 8 bytes of HMAC-SHA-256 over the IP address’s bytes, keyed with the pseudonym key: 4 bytes for IPv4, including an IPv4-mapped IPv6 address, and 16 for IPv6. The port is left out. truncate shows the IPv4 /24 or IPv6 /48.

Container images

Each published release provides ghcr.io/pederbe/udpstp:<version>, such as :0.1.0, which never changes. :latest follows releases that are not marked as pre-releases, including one that ends a deprecation period. Pin :<version> and upgrade on purpose.

Command line

Every option of udpstp client and udpstp server, generated from the binary’s own --help output when the site was built. udpstp client --help and udpstp server --help print the same.

$ udpstp --help
Measure IP-Layer Capacity with UDPSTP (RFC 9946): run a Test as the Client, or
serve Tests as the Server

Usage: udpstp [OPTIONS] <COMMAND>

Commands:
  client  Run a Test against one or more Servers
  server  Serve Tests: answer Clients' Setup and Activation Requests and run
          their Test Connections
  help    Print this message or the help of the given subcommand(s)

Options:
      --color <COLOR>  Terminal styling: auto for capable terminals, always for
                       redirected human output too, or never for plain text
                       [default: auto] [possible values: auto, always, never]
  -h, --help           Print help
  -V, --version        Print version

udpstp client

Runs a Test. Running a Test shows the options in use.

Usage: udpstp client [OPTIONS] <SERVER>...
ArgumentDescription
<SERVER>…The Server's host name or address, optionally with a port (the Standard Port 24601 by default). Several Servers share a Test's Test Connections in turn; they must all be IPv4 or all IPv6
OptionDescription
–color <COLOR>Terminal styling: auto for capable terminals, always for redirected human output too, or never for plain text [default: auto] [possible values: auto, always, never]
–test-connections <N>How many Test Connections the Test runs, 1 to 255 [default: one per SERVER]. OB-UDPST Servers accept at most 24
–min-test-connections <N>The Minimum Test Connections: how many must succeed for the Test to go on [default: all of them]. With all required, one lost Setup Response aborts the Test
–unauthenticatedRun the Test unauthenticated. Not conformant to RFC 9946; the Server must allow it too
–key-file <FILE>The key file: a TOML Key Table if its name ends in .toml, else an OB-UDPST udpst.keys file. Group and others must not be able to read it. Keys are never given on the command line
–key-id <KEYID>Send with this keyId's key, for a key file with several keys valid now
–auth-mode <MODE>The Authentication Mode with a key file: 2 authenticates the Control Phase and every Status PDU (RFC 9946 §5.3), 1 the Control Phase only, for a Server that doesn't support mode 2. The Client never falls back to mode 1 by itself [default: 2]
–mode-1-server <SERVER>Use mode 1 with this SERVER, written as among the SERVERs, for one that doesn't support mode 2. Repeatable
–allow-server-address <SERVER=IP>Authorize an alternate IP address for this SERVER's authenticated Setup Responses. SERVER must match a positional SERVER; IP must be a literal address of the same family. Repeatable. Without this, only the selected address is accepted
–time-limit <SECONDS>Give up sooner than the Test initiation timer's 3 s if the Control Phase hasn't finished after this many seconds [default: 3]
–direction <DIRECTION>The direction of the Test: the Server sends the load (downstream) or the Client does (upstream) [default: downstream] [possible values: downstream, upstream]
–test-interval <SECONDS>The Test Interval to ask the Server for, in seconds (5-60; longer only with an Operator Key that allows it) [default: 10]
–fixed-rate <ROW>Run a Fixed-Rate Test at this Sending Rate Table row. The Server allows it only for an Operator Key
–start-row <ROW>Start the Server's search at this Sending Rate Table row instead of the first (row N offers N Mbit/s up to 1000)
–random-payloadAsk for random payload content instead of zeros, so compression on the path can't inflate the result
–delay-var <DELAY_VAR>The delay variation the Load Rate Adjustment Algorithm judges by Possible values: - one-way: One-way delay variation (useOwDelVar 1): no clock synchronisation needed, but it follows clock drift - rtt: Round-trip time variation (useOwDelVar 0): immune to clock drift [default: one-way]
–refuse-coercion <PARAMETER>End the Test if the Server coerces this Activation parameter, named as in RFC 9946 (e.g. testIntTime). Repeatable
–checksum[=<BOOL>]Use the header checksum (RFC 9946 §5.6): fill it in every PDU sent, verify it in every PDU received when populated [default: false] [possible values: true, false]
–jumbo[=<BOOL>]Ask for Jumbo rows above 1 Gbps (CHSR_JUMBO_STATUS); the Server must serve the same [default: true] [possible values: true, false]
–traditional-mtu[=<BOOL>]Ask for 1500-byte packets (CHSR_TRADITIONAL_MTU); the Server must serve the same [default: false] [possible values: true, false]
–algorithm <ALGORITHM>The Load Rate Adjustment Algorithm the Server searches with [default: b] [possible values: b, c]
–max-bandwidth <MBPS>The most the Test is expected to send or receive, in Mbps (maxBandwidth), shared equally between its Test Connections. The Server searches no higher, and a Server with an Admitted Rate Cap may refuse the Test
–dscp <DSCP>The DSCP to mark the Test's traffic with, 0 to 63, so a specific traffic class is measured. The Server may coerce it to 0 [default: 0]
–format <FORMAT>How to print the results Possible values: - text: Human-readable text - json: JSON compatible with OB-UDPST's client schema; progress goes to stderr [default: text]
–address-privacy <ADDRESS_PRIVACY>How text and JSON results show this Client's own address: in full, pseudonymized with a keyed hash, or truncated to the IPv4 /24 or IPv6 /48 Possible values: - full: The address and port as they are - pseudonymize: anon- and the first 8 bytes of HMAC-SHA-256 over the IP address, in hex, keyed with the pseudonym key: one client keeps one pseudonym while the key is kept, and no one without it can reverse it - truncate: The IPv4 /24 or IPv6 /48 the address is in, without the port [default: full]
–address-privacy-key-file <FILE>The pseudonym key file, hexadecimal digits (16 bytes or more), for –address-privacy pseudonymize
–json-extension[=<BOOL>]With –format json, add udpstp's own results (the Performance Criterion, the Maximum and Highest IP-Layer Capacity, warnings, how the Test ran) as a last "udpstp" object; false leaves OB-UDPST's keys only [default: true] [possible values: true, false]
–max-loss-ratio <RATIO>The Performance Criterion: the most a Sub-Interval may lose, as a share of the Load PDUs sent, to count towards the Maximum IP-Layer Capacity (RFC 9097 §6.3). One lost Load PDU always passes; a Sub-Interval in which none arrived never does [default: 0.01]
–faster-sub-interval-margin <PERCENT>Warn when, in a Search Test, a Sub-Interval after the Maximum IP-Layer Capacity's ran more than this many percent faster and none after it met the Performance Criterion: the Maximum may then be well below the path, as a search through a drop-tail queue gives [default: 5]
–first-mode-sub-intervals <N>Put the first N Sub-Intervals in a Capacity Mode of their own, with its own Maximum IP-Layer Capacity, for a path whose capacity changes on a schedule (RFC 9097 §6.6)
–socket-buffer-size <BYTES>The Data Phase's receive and send buffer, in bytes; the OS's limits may hold it lower [default: 2097152]
–dont-fragment[=<BOOL>]Set the Don't Fragment bit on test traffic; false lets the path fragment it [default: true] [possible values: true, false]
–max-hops <HOPS>MaxHops: the IPv4 TTL or IPv6 hop limit of test traffic, from the expected path length [default: 64]
-h, –helpPrint help (see a summary with '-h')

udpstp server

Serves Tests. Every option also has a setting in the configuration file (Server configuration); an option given on the command line replaces the file’s value. Hosting a Server shows them in use.

Usage: udpstp server [OPTIONS]
OptionDescription
–color <COLOR>Terminal styling: auto for capable terminals, always for redirected human output too, or never for plain text [default: auto] [possible values: auto, always, never]
–config <FILE>The Server's TOML configuration file. Command-line options override its settings
–listen <ADDRESS>An address to listen on for Setup Requests, instead of the IPv4 and IPv6 wildcards. Repeatable
–port <PORT>The UDP port to listen on for Setup Requests (the Standard Port 24601 by default)
–allow-unauthenticated[=<BOOL>]Serve unauthenticated Tests too. Not conformant to RFC 9946; Clients must opt in as well [possible values: true, false]
–checksum[=<BOOL>]Use the header checksum (RFC 9946 §5.6) [possible values: true, false]
–jumbo[=<BOOL>]Serve Jumbo rows above 1 Gbps (CHSR_JUMBO_STATUS) [default: true] [possible values: true, false]
–traditional-mtu[=<BOOL>]Serve 1500-byte packets (CHSR_TRADITIONAL_MTU) [default: false] [possible values: true, false]
–server-mtu <BYTES>The Server MTU: the largest IP packet the Server sends, in bytes [default: 1500]
–ephemeral-ports <FIRST-LAST>The Server's Ephemeral Ports for Test Connections, as FIRST-LAST, picked at random; its own range, not the OS's [default: 49152-65535]
–delay-var-statistic <STATISTIC>Which statistic of a Trial Interval's one-way delay variation classifies it [default: maximum] Possible values: - average: The Trial Interval's average, as OB-UDPST uses - maximum: The Trial Interval's maximum: the delay range, which the RFCs judge
–tests-per-source <N>How many Tests may run at once from one Source [default: 1, as RFC 9946 §4.2 requires]
–max-test-connections <N>How many Test Connections activated Tests may reserve at once [default: 256]. The Ephemeral Port range must hold this plus the unactivated share
–test-connections-per-test <N>How many Test Connections one Test may have over its life [default: 24]
–max-unactivated-test-connections <N>How many Test Connections may be admitted but not yet activated at once, separately from the global cap [default: half of –max-test-connections, at least –test-connections-per-test]
–admitted-rate-cap <MBPS>The Admitted Rate Cap: the most maxBandwidth admitted at once in each direction, in Mbps [default: no cap]. A Test over it is refused
–max-test-share <PERCENT>The most of the Admitted Rate Cap, in percent, that one Test without an Operator Key may state [default: 50]
–require-max-bandwidth[=<BOOL>]Refuse Tests that state no maxBandwidth [possible values: true, false]
–troubleshooting[=<BOOL>]Answer authentication failures with the reason instead of silence, to debug Clients; up to 10 a second. Anyone can then learn whether keys are configured, and spoofed requests get replies [possible values: true, false]
–unverified-setups-per-second <N>How many Setup Requests a second may reach authentication; the rest are dropped silently [default: 20000]
–source-setup-burst <N>How many authentic Setup Requests one Source may send at once [default: 48]; at least –test-connections-per-test
–source-setups-per-second <N>How many more a Source may send each second after its burst [default: 5]
–key-setup-burst <N>How many authentic Setup Requests one Key Table row may send at once, Unauthenticated Mode counting as one [default: 1000]; at least –test-connections-per-test
–key-setups-per-second <N>How many more a Key Table row may send each second after its burst [default: 1000]
–allowed-dscp <DSCP,…>…The DSCPs a Test without an Operator Key may mark its traffic with, comma-separated [default: 0]; another is coerced to 0
–operator-allowed-dscp <DSCP,…>…The DSCPs a Test with an Operator Key may mark its traffic with, comma-separated [default: all but network control, 48 and 56]
–max-start-row <ROW>The highest Sending Rate Table row a Test without an Operator Key may start its search at [default: 100]; Operator Keys are exempt
–quota-period <SECONDS>The Quota Period Quotas are counted over, in seconds [default: 3600]
–source-tests-per-quota-period <N>Each Source's Quota of Tests per Quota Period [default: none]; Operator Keys are exempt
–source-bytes-per-quota-period <BYTES>Each Source's Quota of load bytes per Quota Period, both directions [default: none]; Operator Keys are exempt
–socket-buffer-size <BYTES>Each Test Connection's receive and send buffer in its Data Phase, in bytes [default: 2 MiB]; the OS's limits may hold it lower
–dont-fragment[=<BOOL>]Set the Don't Fragment bit on test traffic [default: true]; false lets the path fragment it [possible values: true, false]
–max-hops <HOPS>MaxHops: the IPv4 TTL and IPv6 hop limit of test traffic, from the expected path length (RFC 9946 §10 says the operator MUST set it) [default: 64, with a warning]
–log-format <FORMAT>The log's format on stdout [default: human]; json writes one object a line Possible values: - human: Readable events, with indented continuation lines when needed - json: One JSON object a line
–log-file <FILE>Where the log goes instead of stdout, with the startup's errors and warnings: a new file each day, the date before the extension, the last 14 kept
–address-privacy <HOW>How the log shows client addresses [default: full]: pseudonymize with a keyed hash, or truncate to the IPv4 /24 or IPv6 /48 Possible values: - full: The address and port as they are - pseudonymize: anon- and the first 8 bytes of HMAC-SHA-256 over the IP address, in hex, keyed with the pseudonym key: one client keeps one pseudonym while the key is kept, and no one without it can reverse it - truncate: The IPv4 /24 or IPv6 /48 the address is in, without the port
–address-privacy-key-file <FILE>The pseudonym key file, hexadecimal digits (16 bytes or more), for –address-privacy pseudonymize
–statistics-interval <SECONDS>How often the log reports the Server's counters, in seconds [default: 60]; 0 never does
–key-file <FILE>The key file: a TOML Key Table if its name ends in .toml, else an OB-UDPST udpst.keys file. Group and others must not be able to read it
–print-configValidate the settings and key file, print effective TOML, and exit
–settings-onlyWith –print-config, preview settings without opening keys or logs
–windows-serviceRun as the Windows service the Service Control Manager started, which stops it, logging to log_file (Windows only)
-h, –helpPrint help (see a summary with '-h')

Server configuration

The Server reads its settings from built-in defaults, then a TOML file (udpstp server --config FILE), then the command line, each replacing the one before. This page is generated from the settings’ own documentation and from udpstp server --print-config, which prints the effective settings as TOML and exits.

Defaults

With no file and no options, --print-config prints:

listen = []
port = 24601
allow_unauthenticated = false
checksum = false
jumbo = true
traditional_mtu = false
server_mtu = 1500
ephemeral_ports = "49152-65535"
delay_var_statistic = "maximum"
tests_per_source = 1
max_test_connections = 256
test_connections_per_test = 24
max_test_share = 50
require_max_bandwidth = false
troubleshooting = false
unverified_setups_per_second = 20000
source_setup_burst = 48
source_setups_per_second = 5
key_setup_burst = 1000
key_setups_per_second = 1000
allowed_dscp = [0]
max_start_row = 100
quota_period = 3600
socket_buffer_size = 2097152
dont_fragment = true
log_format = "human"
address_privacy = "full"
statistics_interval = 60

A setting shown as “none” is absent by default and printed only when set.

Settings

SettingDefaultMeaning
listen[]The addresses the Server listens on for Setup Requests; empty, the IPv4 and IPv6 wildcards (IPv6 if the host has it). A listener on a specific address never sees multicast or broadcast Setup Requests.
port24601The UDP port the Server listens on for Setup Requests.
allow_unauthenticatedfalseServe unauthenticated Tests too (not conformant to RFC 9946).
checksumfalseFill the header checksum and verify it when populated (RFC 9946 §5.6).
jumbotrueCHSR_JUMBO_STATUS: rows above 1 Gbps may use packets up to 9000 bytes (RFC 9946 §6.1). Clients must ask for the same.
traditional_mtufalseCHSR_TRADITIONAL_MTU: packets may be 1500 bytes, not 1250, up to 1 Gbps too. Clients must ask for the same.
server_mtu1500The Server MTU: the largest IP packet the Server sends, in bytes, 1280 or more (9000 at most is used). Jumbo rows keep their rates with more, smaller packets below 9000.
ephemeral_ports"49152-65535"The Ephemeral Ports Test Connections' sockets are bound to, as FIRST-LAST, picked at random: the Server's own range, not the OS's ephemeral port range. It holds at least max_test_connections plus the Unactivated Test Connection share.
delay_var_statistic"maximum"Which statistic of a Trial Interval's one-way delay variation classifies it for the Load Rate Adjustment Algorithms.
tests_per_source1How many Tests may run at once from one Source (an IPv4 address or an IPv6 /64). RFC 9946 §4.2 says 1; more, for Clients behind carrier-grade NAT, departs from it.
max_test_connections256How many Test Connections activated Tests may reserve at once.
test_connections_per_test24How many Test Connections one Test may have over its life.
max_unactivated_test_connectionsnoneHow many Test Connections may be admitted but not yet activated at once, separately from the global cap; absent, half of max_test_connections, but at least test_connections_per_test.
admitted_rate_capnoneThe most maxBandwidth admitted at once in each direction, in Mbps; no cap when absent.
max_test_share50The most of admitted_rate_cap, in percent, that one Test without an Operator Key may state, all its Test Connections together.
require_max_bandwidthfalseRefuse Tests that state no maxBandwidth.
troubleshootingfalseAnswer authentication failures of Setup Requests with the reason instead of silence, to debug Clients. Off by default.
unverified_setups_per_second20000How many Setup Requests a second may reach authentication; the rest are dropped silently.
source_setup_burst48How many authentic Setup Requests one Source may send at once; at least test_connections_per_test.
source_setups_per_second5How many more a Source may send each second after its burst.
key_setup_burst1000How many authentic Setup Requests one Key Table row may send at once, Unauthenticated Mode counting as one; at least test_connections_per_test.
key_setups_per_second1000How many more a Key Table row may send each second after its burst.
allowed_dscp[0]The DSCPs, 0 to 63, a Test without an Operator Key may mark its traffic with; another is coerced to 0. Only 0 by default, so a Consumer can't put the load into a priority class.
operator_allowed_dscpnoneThe DSCPs a Test with an Operator Key may mark its traffic with; absent, all but network control (48 and 56).
max_start_row100The highest Sending Rate Table row a Test without an Operator Key may start its search at; a higher one is coerced to it. Operator Keys are exempt.
quota_period3600The Quota Period, in seconds: what Quotas are counted over.
source_tests_per_quota_periodnoneEach Source's Quota of Tests per Quota Period; absent, none. Operator Keys are exempt.
source_bytes_per_quota_periodnoneEach Source's Quota of load bytes per Quota Period, both directions; absent, none. Operator Keys are exempt.
socket_buffer_size2097152Each Test Connection's receive and send buffer in its Data Phase, in bytes, within the OS's limits.
dont_fragmenttrueSet the Don't Fragment bit on test traffic (RFC 9946 §5: it SHOULD be set unless fragmentation is being measured).
max_hopsnoneMaxHops: the IPv4 TTL and IPv6 hop limit of test traffic, 1 to 255, set from the expected path length (RFC 9946 §10). Absent, 64, with a warning.
log_format"human"The log's format on stdout: human or json, one object a line.
log_filenoneWhere the log goes instead of stdout, with the startup's errors and warnings: a new file each day, named from this path with the date before its extension (server.log gives server.2026-10-01.log), the last 14 kept. A Windows service has no stdout, so it needs one.
address_privacy"full"How the log shows client addresses: full, pseudonymize or truncate (to the IPv4 /24 or IPv6 /48).
address_privacy_key_filenoneThe pseudonym key, as hexadecimal digits (16 bytes or more), for address_privacy = "pseudonymize".
statistics_interval60How often the log reports the Server's counters, in seconds, up to a day; 0 never does.
key_filenoneThe key file: a TOML Key Table if its name ends in .toml, else an OB-UDPST udpst.keys file. A relative path is taken from the current directory.

Hosting a Server explains the settings in use, and Command line lists the option for each.

Server log

The Server logs one line per event: what it admitted, refused and measured. This page lists every event, field and reason. Hosting a Server shows how to set the log up.

Event names, field keys and the values below settle during 0.x and become stable from 1.0, as Stable interfaces explains. New ones may appear, so ignore those you don’t know.

Format

The log is human-readable by default:

2026-09-26T14:46:25.579954Z  INFO listening on 0.0.0.0:24700 event="Listening" address=0.0.0.0:24700
2026-09-26T14:46:26.621011Z  INFO Test Connection admitted event="TestConnectionAdmitted" test_connection_id=1 client="127.0.0.1:51801" mc_ident=60798
2026-09-26T14:46:26.621208Z  INFO Test Connection activated event="TestConnectionActivated" test_connection_id=1 direction="Downstream"
2026-09-26T14:46:37.129642Z  INFO the Test Connection's Data Phase completed event="DataPhaseEnded" test_connection_id=1 reason="Completed" fast_paths="sendmmsg,UdpGso,recvmmsg,KernelRxTimestamps,epoll"
2026-09-26T14:46:37.129734Z  INFO Test ended: Completed event="TestSummary" client=127.0.0.1:51801 mc_ident=60798 test_connections=1 direction="Downstream" test_interval=10 … bytes_sent=1144801422 bytes_received=1110936104 ip_layer_sender_bit_rate=933913704 result="Completed"

With log_format = "json", each line is one JSON object:

{"timestamp":"2026-09-26T14:51:10.061951Z","level":"INFO","message":"Setup Request refused","event":"SetupRequestRejected","client":"anon-63d4269f2e4b0c0d","reason":"MaxBandwidthRequired"}

Every event has an event name and its data in fields. A field means one thing wherever it appears, and is left out when an event has nothing for it. The message is for people and may change, so filter on event and the fields.

In a terminal, the human log colours its level labels: INFO cyan, WARN yellow and ERROR red. --color auto|always|never controls this, as Terminal color describes. JSON logs and log files are always plain.

Where it goes

The log goes to standard output, and startup warnings and errors to standard error. With log_file set, the Server writes a new file each day named from it, so /var/log/udpstp/server.log gives server.2026-10-01.log (the date in UTC). It keeps the last 14, and copies its startup errors and warnings into them as StartupError and StartupWarning.

Give log_file a folder of its own: the Server deletes the oldest file whose name starts and ends as the log’s does, whatever wrote it. A Windows service always logs to files. Elsewhere, leave the log to the service manager: logrotate’s renames would fight the Server’s own files, and SIGHUP stops the Server rather than reopening them.

Events an attacker can cause are rate-limited, ten of a kind every 10 s, so a flood can’t fill the disk. The rest are counted in a Suppressed event.

Fields

FieldTypeWhat
test_connection_idintegerThe Test Connection, the same in each of its events; left out for the Standard Port
clientstringThe Client’s address, as address_privacy shows it
mc_identintegerThe Test’s mcIdent. With client, it identifies the Test
key_idintegerThe keyId of the Key Table row that authenticated the Test
reasonstringWhy, a name from a fixed set (below)
countintegerHow many times it happened since the last such event
errorstringThe operating system’s error, in its words: free text
directionstringUpstream or Downstream
parameter, requested, coercedstring, integer, integerAn Activation parameter by its RFC 9946 name, the value the Client asked for, and the value the Server gave
rowintegerA Sending Rate Table row
requested_bytes, receive_bytes, send_bytesintegerSocket buffer sizes: asked for, and what the OS gave
bursts, datagrams, bytesintegerBursts not sent, their Load PDUs, and their UDP payload bytes
executor, engine_threads, grouping_usstring, integer, integerHow the Data Phase Engines run (DataPhaseEngines)
fast_pathsstringThe fast paths a Test Connection used (DataPhaseEnded)
receive_buffer_overflowsintegerOn Linux, datagrams the OS discarded at the socket before the Server read them, mostly for a full receive buffer
send_lag_samples, send_lag_p50_us, send_lag_p99_us, send_lag_max_us, burst_lateness_p99_usintegerExperimental send timing, in µs (DataPhaseEnded)

Events

Starting and stopping

EventWhenFields
DataPhaseEnginesAt start: how the Data Phase Engines run (below)executor, grouping_us, engine_threads, error
ListeningThe Server listens on an addressaddress
NoIpv6ListenerThe host has no IPv6, so the Server listens on IPv4 aloneerror
TroubleshootingModeOnAt start, and every minute while Troubleshooting Mode is on
StartupErrorWith log_file set: the Server couldn’t starterror, the message (free text); exit_status
StartupWarningWith log_file set: a warning at startupreason: DscpAskedAsZero, DscpCoercedToZero, DontFragmentUnsupported, AdmittedRateCapWithoutMaxBandwidth, NoTestAdmissible, TroubleshootingModeOn, SocketBuffersShort, SocketBuffersUnknown, MaxHopsNotSet or TestsPerSource
StoppingThe Server was asked to stop. It ignores new Setup Requests, sends STOP to its active Test Connections and ends themreason: SIGTERM, SIGINT or SIGHUP; on Windows CtrlC, CtrlBreak, CtrlClose or CtrlShutdown; as a Windows service ServiceStop or ServiceShutdown

Setup and Activation

EventWhenFields
TestConnectionAdmittedA Setup Request was acceptedtest_connection_id, client, mc_ident, and key_id in an authenticated mode
SetupRequestRejectedA Setup Request was refused with a Setup Responseclient, reason
SetupRequestSilentlyRejectedA Setup Request was refused without a response (Silent Rejection)client, reason
SetupRequestIgnoredA Setup Request was ignored: DuplicateSetupRequest, a copy of one already answered; ArrivalAddressUnknown, which the Client retries; or ServerStoppingclient, reason
TestConnectionActivatedAn Activation Request was acceptedtest_connection_id, direction
ActivationParameterCoercedThe Activation Response changed a parametertest_connection_id, parameter, requested, coerced
ActivationRequestRejectedAn Activation Request was refusedtest_connection_id, reason, and parameter for UnacceptableParameter
ActivationRequestSilentlyRejectedAn Activation Request was refused without a responsetest_connection_id, reason
ActivationWatchdogExpiredNo valid Activation Request came within 3 stest_connection_id
UnactivatedTestConnectionEvictedThe Unactivated Test Connections filled their share, so the oldest of the Key Table row holding the most was closed to make roomtest_connection_id

The Data Phase

EventWhenFields
FirstLoadPduReceived, FirstStatusPduReceived, StopReceivedThe first Load PDU or Status PDU, and the STOP exchangetest_connection_id
BurstsSkippedThis host fell behind sending load, so the Server, not the path, limited the Testtest_connection_id, bursts, datagrams, bytes
BurstsWithheldThe send buffer was fulltest_connection_id, bursts, datagrams, bytes
InvalidLoadPdus, IgnoredLoadPdus, InvalidStatusPdus, StaleStatusPdus, StatusPdusFailedVerification, StatusPdusMissing, PeerMissedStatusPdus, PeerRxStopped, DelaySamplesDropped, IcmpErrorsPDUs ignored or missing, and ICMP errors, countedtest_connection_id, count
PathMtuExceeded, HopLimitExceededDatagrams too large for the path with DF set, or expired on the way because max_hops is too smalltest_connection_id, count
LostStatusBackoffNo Status PDU came in time, so a Downstream Search Test backed off as for an Errored Intervaltest_connection_id, row
TrafficStoppedNothing arrived for 1 s, so this end stopped sendingtest_connection_id
DataPhaseEndedA Test Connection’s Data Phase ended (below)test_connection_id, reason, fast_paths, receive_buffer_overflows, error
TestConnectionReleasedA Test Connection ended and its Admission was releasedtest_connection_id
TestSummaryA Test ended (below)

Problems and counts

EventWhenFields
SocketError, NoEphemeralPortA socket failed, or no Ephemeral Port was freetest_connection_id (none for the Standard Port), error
SocketBuffersShortThe OS held a Test Connection’s socket buffers below socket_buffer_sizetest_connection_id, requested_bytes, receive_bytes, send_bytes
DatagramsDroppedThe Server’s Control Phase fell behind and discarded datagrams its socket readers had received. This is udpstp’s own queue, not a Receive Buffer Overflowtest_connection_id (none for the Standard Port), count
Statistics, StatisticsRefusalsEvery statistics_interval seconds, and once more when the Server stops (below)
SuppressedEvents of one kind beyond ten every 10 s, counted rather than loggedsuppressed_event, count, and the sums of what they carried: bursts, datagrams and bytes, or suppressed_count for events with a count

DataPhaseEngines

executor is pool on Linux, where Engine Threads each serve many Test Connections, or per_test_connection on Windows and macOS, where each Test Connection has an Engine Thread of its own.

For pool, engine_threads is the most it starts: one for each CPU the process may use, with a fractional cgroup CPU quota rounded down, so a container limited to 1.9 CPUs gets 1. error is a warning when the CPUs the process may use couldn’t be found.

grouping_us is the pool’s wake-up grouping window, 0 by default and for per_test_connection. The experimental variables UDPSTP_ENGINE_THREADS and UDPSTP_ENGINE_GROUPING_US override engine_threads and grouping_us.

DataPhaseEnded

reason says how the Data Phase ended (below). error names the failure when the Data Phase Engine failed.

fast_paths lists, comma-separated, the fast paths the engine sent Load PDUs, read datagrams and waited with. It is empty except on Linux, where it can hold:

  • sendmmsg and UdpGso when the Server sent Load PDUs (not in an Upstream Test);
  • recvmmsg and KernelRxTimestamps;
  • epoll when one of the Server’s Engine Threads served the Test Connection.

With the experimental UDPSTP_KERNEL_TX_TIMESTAMPS=1 on Linux 6.13 and later, each message goes with a sendmsg call of its own, so UdpGso appears without sendmmsg, unless the kernel refuses the timestamps. A Load Sender’s event then adds send_lag_samples, send_lag_p50_us, send_lag_p99_us and send_lag_max_us: how much later than its lpduTime the kernel sent the Load PDUs it stamped. burst_lateness_p99_us, also experimental, is how late a Load Sender wrote its Send Plans, 99th percentile.

receive_buffer_overflows, on Linux, counts datagrams the OS discarded at the socket before the Server read them. In an Upstream Test the Client counts them as lost.

TestSummary

A TestSummary has:

  • client, mc_ident, key_id (in an authenticated mode) and test_connections;
  • bytes_sent and bytes_received, the UDP payload bytes of the Load PDUs the Server sent and received;
  • ip_layer_sender_bit_rate, in bit/s, when the Server was the Load Sender;
  • what the Server didn’t send as Load Sender, over all its Test Connections: skipped_bursts, skipped_datagrams and skipped_bytes when it fell behind, and withheld_bursts, withheld_datagrams and withheld_bytes when the send buffer was full;
  • receive_buffer_overflows, on Linux, summed over its Test Connections;
  • result: Completed; ServerStopping for a Test cut short when the Server stopped; how the first Test Connection that didn’t complete ended (a DataPhaseEnded reason, ActivationWatchdogExpired, UnactivatedTestConnectionEvicted, or ActivationRequestRejected with its reason); or NotActivated.

A Test that was activated adds its direction and parameters: test_interval (s), sub_interval and trial_interval (ms), low_thresh and upper_thresh (ms), seq_err_thresh, slow_adj_thresh, high_speed_delta, ignore_ooo_dup, one_way_delay_variation and random_payload (booleans), rate_adj_algo (B or C), dscp, sr_index_conf and sr_index_is_start.

Statistics

Statistics counts since the Server started: tests, test_connections, active_test_connections, authentication_failures (requests, and Status PDUs in mode 2), rate_limited (Setup Requests over a budget), stale_status_pdus, setup_requests_ignored and unactivated_test_connections_evicted.

It has a report number. One StatisticsRefusals line with the same report follows for each kind of refusal counted so far, with kind (the event that logged it), its reason and count. The counts overlap: a request refused for its authentication counts in authentication_failures and in its StatisticsRefusals, and one over a budget in rate_limited too.

Reasons

  • For DataPhaseEnded, how it ended: Completed, Unconfirmed (the Client didn’t confirm the STOP), StoppedByClient, Watchdog, OverRate (the Client sent above 1.5 times its rate), Stopped (the Server stopped it) or DataPhaseEngineFailed.
  • For a Setup Request: TestsPerSource, TestConnectionsPerTest, TestConnections, MaxBandwidthRequired, CapacityExceeded, SocketUnavailable, QuotaExceeded, SourceBudgetExhausted, BadVersion, JumboMismatch, TraditionalMtuMismatch, Malformed, MulticastOrBroadcast, ChecksumFailed, Replayed, ReplayCacheFull, UnverifiedBudgetExhausted, or a Multi-Connection field’s fault: MultiConnectionIdentZero, MultiConnectionIndexNotBelowCount, MultiConnectionIndexInUse, MultiConnectionCountDiffers, MultiConnectionAuthModeDiffers, MultiConnectionKeyIdDiffers.
  • For an Activation Request: InvalidCommand, MixedDirection, UnacceptableParameter, FixedRateNeedsOperatorKey, TestsPerSource, TestConnections, MaxBandwidthDirection, CapacityExceeded, QuotaExceeded, AuthModeMismatch, KeyIdMismatch, MulticastOrBroadcast, ChecksumFailed, Replayed, ReplayCacheFull, NotFromClient, Malformed.
  • For an authentication failure, whether Troubleshooting Mode answered it or not: AuthenticationRequired, AuthenticationNotConfigured, AuthenticationModeInvalid, AuthenticationUnknownKeyId, AuthenticationKeyOutsideLifetime, AuthenticationDigestFailed, AuthenticationTimeOutOfWindow.

JSON results

udpstp client --format json prints the Test’s results as JSON compatible with OB-UDPST v9.1.0’s client output: the same keys, in the same order, with numbers and times printed the way OB-UDPST prints them, so tools written for OB-UDPST’s formatted JSON (-f jsonf) keep working. Progress goes to stderr, so stdout holds only the JSON.

From 0.1.0 the key names, types and enumerated values are a stable interface, the extension’s included; the free text in ErrorMessage, ErrorMessage2 and a warning’s Message is not.

What is the same

  • Top-level keys: IPLayerMaxConnections, IPLayerMaxIncrementalResult, IPLayerCapSupported, then Input and Output once the Test was activated, then ErrorStatus, ErrorMessage and ErrorMessage2.
  • Output: BOMTime to TimestampResolutionUsed, then IncrementalResult, Summary, ModalSummary, AtMax, ModalResult, ECNBleachingDetected, then EOMTime and Status.
  • Printed forms: delays in seconds from whole milliseconds (0.011), Mbps and percentages with 2 decimals, ratios with 9, zero as 0.0, times as UTC with microseconds (2026-09-25T14:13:20.712759Z), and OB-UDPST’s tab-indented layout.
  • A Test that never activated has only the top-level IPLayer* keys and the error fields; one that didn’t complete (ErrorStatus 200 and up) has only Output’s header and trailer.

Tests compare the key paths, their order and each value’s printed form with OB-UDPST’s vendored examples 01, 06, 11 and 12 (testdata/obudpst-9.1.0/json_examples).

Fields udpstp never measures

They carry OB-UDPST’s own value for the feature being off:

ValueFields
0 or 0.0CECountOfDelivered, CEPercentOfDelivered (every object), ECNBleachingDetected, Input.ECN, Input.CEThreshold
"", 0.0 or 0Input.Interface, InterfaceEthMbps (every object), Input.InterfaceDeterminesMax
fixedNumberFirstModeSuppSubIntervals 0, PreambleDuration 0, IPRREnable 1, RIPREnable 1, TimestampResolution and TimestampResolutionUsed 1, Tmax, TmaxUsed, TimeoutNoTestTraffic and TimeoutNoStatusMessage 1000 (OB-UDPST’s 1 s traffic-stopped timer, not RFC 9097’s Tmax), TmaxRTT and TmaxRTTUsed 3000, ProtocolVersion "Any"; JumboFramesPermitted, UDPPayloadMax and UDPPayloadDefault follow --jumbo and --traditional-mtu
Multi-ConnectionNumberOfConnections and MinNumOfConnections from --test-connections and --min-test-connections, NumberOfHosts and HostList from the SERVERs; a Downstream Test’s Sub-Intervals are its Test Connections’ summed on one grid, where OB-UDPST sums them by index, so the figures can differ from OB-UDPST’s; ActiveConnections counts the Test Connections that received in the Sub-Interval (1 with one Test Connection, as OB-UDPST), in a Summary in its Capacity Mode’s last; MinOnewayDelay is 0 when the Test Connections go to several Servers, whose clocks the Client can’t relate. An Upstream Test’s Sub-Intervals are each Server’s own, summed by number, so they are approximate: a warning in the udpstp extension, not ErrorMessage, gives the largest offset between them

ClientIPAddress is "" today: the Client’s socket is bound to a wildcard address, which says nothing about the one used. When it holds an address, it follows --address-privacy (below).

Where udpstp differs

Each is an OB-UDPST Difference with its own Conformance Record.

  • AtMax and ModalResult are the Maximum IP-Layer Capacity, which meets the Performance Criterion. OB-UDPST’s maximum, the Highest IP-Layer Capacity, is in the extension. A Capacity Mode with no maximum has no AtMax (or ModalResult), never zeros, and the Test gets ErrorStatus 49.
  • ErrorStatus is the most severe status, where OB-UDPST keeps the last one written: 200 for a Test that didn’t complete, with its cause in ErrorMessage2; for a completed Test the most severe warning: 49 no valid maximum, 4 nothing received for 1 s, 5 the peer stopped receiving, 3 the peer missed Status PDUs, 2 Status PDUs missing here, 6 invalid PDUs. The exit status of a completed Test is 0 all the same.
  • An interrupted Test (Ctrl-C, a signal to stop, or a failed write to standard output while the Test ran) has ErrorStatus 204 and the results so far, its Test Connections having sent their STOP; OB-UDPST stops without one and exits 255.
  • A Test that didn’t complete keeps its measured Sub-Intervals in udpstp.SubIntervals, with the same fields and units as Output.IncrementalResult, and its minimum RTT in udpstp.MinRTT. Output still has only its header and trailer. These partial results require the extension, which is on by default.
  • SoftwareVersion is "udpstp <version>"; ControlProtocolVersion is 20.
  • Delay fields of a Sub-Interval without samples are 0, as OB-UDPST writes, and the extension lists those Sub-Intervals; Summary is computed from real samples only.
  • A Sub-Interval never measured is left out of IncrementalResult and listed in the extension; Interval is its subIntSeqNo.
  • IPLayerCapacity is over the nominal Sub-Interval (RFC 9097 §5.3); OB-UDPST divides by the measured one.
  • A name that doesn’t resolve, or a Client that can’t start, prints no JSON; OB-UDPST prints JSON with 255.

The extension

One last object, "udpstp", holds results OB-UDPST’s schema lacks; --json-extension=false leaves it out for tools that reject unknown keys:

KeyWhat
WarningsThe results’ warnings, each {Code, Count, Message}: the text results’ line as Message, which may change; Code, the Server log’s event name for it (such as BurstsSkipped, PathMtuExceeded, TrafficStopped, SocketBuffersShort, or DataPhaseEnded for a Test that ended without the STOP exchange: in a Multi-Connection Test, one for each Test Connection that did, its Message naming it), or SendingRateCapped, FirstModeSplitIgnored, UpstreamResultsApproximate, Interrupted (the Client user stopped the Test), ReceiveBufferOverflows (datagrams this host’s OS discarded at the socket before udpstp read them; Linux only), SendingRateNotKept (a Downstream Fixed-Rate Test whose Server sent more than 5% below its row’s sending rate in the Sub-Interval of a Maximum IP-Layer Capacity, as the Client estimated what it sent: it may not have kept up, so the result may understate the path; its Count is the Capacity Modes it concerns; a Search Test’s Server tells the Client no sending rate), CongestionNeverConfirmed (a Search Test none of whose Test Connections’ searches found the path’s limit, as the Client classified their Trial Intervals, by the maximum delay variation: none confirmed Congestion, slowAdjThresh Errored Intervals back to back, nor met the limit at its end, at least 4 of its last 64 Trial Intervals errored and at least 1 in 4 from the first of those on, as a search cycling around the limit above the 1 Gbps row does, so the result may understate the path: one for each Test Connection, in a Multi-Connection Test its Message naming it, saying why its search fell short, the first that fits (its limits): its IP-Layer Sender Bit Rate was at the maxBandwidth it stated, or reached the top of the Sending Rate Table; delay variation held it in Hold Intervals; the Test ended while it still rose; or else the Load Sender, the Server in a Downstream Test and this host in an Upstream one, stopped rising though few of the last 64 Trial Intervals were Errored Intervals, so it may not have kept up. Only for a completed Search Test whose every activated Test Connection completed), SubIntervalsAfterMaximumMissed (a Search Test in which no Sub-Interval after the Maximum IP-Layer Capacity’s met the Performance Criterion, and one that missed it ran more than --faster-sub-interval-margin faster than the Maximum, after the Maximum’s, as a search cycling around a drop-tail bottleneck gives, or before it, as a search that climbed back slowly after an overshoot, a path that slowed, a policer or shaper that let a burst through at first or, Downstream, a Server that fell behind gives; or the last one ran under 60% of the Maximum, as a queue that kept dropping after it overflowed, a path that slowed or, Downstream, a Server that fell behind gives, so the Maximum may be well below the path, or no longer what it carries: one for each Capacity Mode, its Message giving each such Sub-Interval, whether before or after the Maximum’s, with its IP-Layer Capacity and Loss Ratio; an Upstream Test in which this host left more than 1% of its Load PDUs unsent isn’t judged on the last one or on one before the Maximum’s) or NoValidMaximum; and Count, the datagrams or PDUs it counts, or 1. NoValidMaximum is listed only here
EndReasonHow the Test ended. With one Test Connection, how it ended: Completed, Unconfirmed, TestIntervalElapsed, StoppedByClient, Stopped, Watchdog or OverRate, or DataPhaseEngineFailed, ActivationRejected, ControlPhaseFailed (a time limit too), Abandoned (given up by the Multi-Connection Coordinator), SocketError. A Multi-Connection Test’s is the Test’s own: Completed, or why the Multi-Connection Coordinator aborted it, TooFewTestConnections, ParametersDiffer, TestConnectionEnded (one refused a Coercion or can’t run a parameter), TestConnectionsTimedOut or Interrupted, or NoTestConnectionCompleted when every Test Connection ended otherwise; each Test Connection’s own end is in TestConnections
ControlPhaseFailureWhy the Control Phase failed, when EndReason is ControlPhaseFailed, else null (so always null for a Multi-Connection Test, whose Test Connections each have their own in TestConnections): TestInitiationTimedOut, NoAuthenticationConfigured, NoKeyForSending, AmbiguousKeyForSending, UnacceptableCoercion, UnrunnableParameter, UnknownSetupResponse, UnknownActivationResponse, MulticastOrBroadcastServer, SocketUnavailable, or SetupRejected and the Setup Response’s code: SetupRejectedBadVersion, SetupRejectedJumboMismatch, SetupRejectedTraditionalMtuMismatch, SetupRejectedAuthNotConfigured, SetupRejectedAuthRequired, SetupRejectedAuthModeInvalid, SetupRejectedAuthFailed, SetupRejectedAuthTimeInvalid, SetupRejectedMaxBandwidthRequired, SetupRejectedCapacityExceeded, SetupRejectedMultiConnectionInvalid, SetupRejectedConnectionAllocationFailure
CoercionsEach parameter the Server coerced, {McIndex, Parameter, Requested, Coerced}: the Test Connection, the parameter’s RFC 9946 name as --refuse-coercion takes it, and the values as the Activation Request and Response carry them (dscpEcn whole, the DSCP shifted left by 2)
DontFragment, SocketBuffers, DataPhaseEngineHow this host ran the Test. DataPhaseEngine is portable. In a Multi-Connection Test, SocketBuffers is the smallest the OS granted any of its Test Connections, receive and send each
FastPathsThe fast paths this host sent Load PDUs, read datagrams and waited with, in this order: sendmmsg (Load PDUs sent in batches), UdpGso (a Burst’s Load PDUs as one message the kernel splits), recvmmsg (datagrams read in batches, Load PDUs header-only), KernelRxTimestamps (datagrams timed by the kernel’s receive timestamps rather than the read) and ppoll (the Data Phase Engine waited on its socket between turns, so an arrival woke it, rather than sleeping). Linux only; empty elsewhere. sendmmsg and UdpGso are missing in a Downstream Test (the Client sends no Load PDUs) and where the kernel refused sendmmsg; UdpGso also where the kernel or device refused it, or no Burst held more than one Load PDU. With UDPSTP_KERNEL_TX_TIMESTAMPS=1 on Linux 6.13 and later sendmmsg is missing too, since each message then goes with a sendmsg call of its own to carry the number of its send timestamp, so UdpGso can come without it; if the kernel refuses the numbers after all, the batches after that go in sendmmsg calls, and it is named. With UDPSTP_KERNEL_TX_TIMESTAMPS=kernel-count the messages carry no numbers, and sendmmsg is named as without the variable. recvmmsg, KernelRxTimestamps and ppoll are missing where the kernel refused them. In a Multi-Connection Test, the fast paths every Test Connection that completed used, so a fallback on any one shows; if none completed, every one whose Data Phase Engine ran until its Data Phase ended. A Test Connection whose engine failed or never ran, or that ended early, is left out: it used none, or not all it would have
KernelSendTimestampsOnly with UDPSTP_KERNEL_TX_TIMESTAMPS set to 1 or kernel-count on Linux, in a Test where the Client is the Load Sender: how much later than their lpduTime the kernel sent the Load PDUs, from its software send timestamps, one per message (a UDP GSO message counts once): Samples, and LagP50Us, LagP99Us and LagMaxUs in µs. On Linux 6.13 and later each message carries its own number, so a send the kernel refuses costs only its own sample; each message then goes with a sendmsg call of its own, and FastPaths doesn’t name sendmmsg. A kernel that refuses the numbers after all (EINVAL) gets its messages without them from then on, its batches in sendmmsg calls, which FastPaths then names, and counting stops there, the Test going on. Before 6.13 the kernel numbers the messages, and may have numbered one it refused (a full queue, a refused UDP GSO send), so counting stops at the first send refused, whole or in part, and a Test with refused sends reports fewer samples. With kernel-count the kernel numbers them on every kernel, as before 6.13, so a result can be checked on that send path: batches in sendmmsg calls, counting stopping at the first send refused. In a Multi-Connection Test, its Test Connections’ samples counted together, with the largest of their lags. Experimental: not a stable interface (interfaces.md)
BurstLatenessIn a Test where the Client is the Load Sender: how late after their scheduled time it wrote its Send Plans, and so stamped their lpduTime, its Burst lateness: Samples, one a Send Plan, and P50Us, P99Us and MaxUs in µs, counted in 1 µs buckets up to 10 ms. In a Multi-Connection Test, its Test Connections’ samples counted together, with the largest of their percentiles. The Server’s log reports its own as burst_lateness_p99_us in DataPhaseEnded. Experimental: not a stable interface (interfaces.md)
PerformanceCriterionMaxLossRatio
CapacityModesEach Capacity Mode’s Sub-Intervals, whether it has a valid result, its Maximum and Highest IP-Layer Capacity
SubIntervals, MinRTTOnly for a Test that activated but didn’t complete, including an interrupted Test. SubIntervals holds every measured Sub-Interval with the same fields as Output.IncrementalResult, preserving its Interval number, loss, delay variation, RTT variation and IP-Layer Capacity. It is empty if none was measured. MinRTT is the smallest measured minimum RTT, in seconds, or null if unavailable. Neither field is a completed-Test summary
MissingSubIntervals, NoSamplesSub-Intervals never measured, and those without delay or RTT samples
IPLayerSenderBitRatePer Sub-Interval, where the Client was the Load Sender; in a Downstream Test IPLayerSenderBitRateEstimate instead, the Client’s estimate of what the Server sent in each st of the Server’s clock, from the lpduSeqNo span of the Load PDUs that arrived, a lost one as big as the st’s others
TestConnectionsA Multi-Connection Test’s Test Connections, each on its own, as the text results show them: {McIndex, Server, EndReason, ControlPhaseFailure, CapacityModes}. A Test with one Test Connection has none

Not yet

  • Downstream output follows OB-UDPST’s code, where only Role changes; every OB-UDPST example is an Upstream Test, so a Downstream Test’s full structure hasn’t been compared with OB-UDPST’s.
  • OB-UDPST’s brief (jsonb) and summary-only (-s) forms.

ClientIPAddress follows --address-privacy: the address in full (the default), anon- and a keyed hash of it (pseudonymize, with --address-privacy-key-file), or its IPv4 /24 or IPv6 /48 (truncate).

Exit statuses

udpstp client exits with the ErrorStatus that OB-UDPST reports, except that a Test that completed with only warnings exits 0. In JSON results the same number is ErrorStatus, warnings included. Generated from the documentation of crates/udpstp/src/exit.rs.

ErrorStatusMeaning
0The Test completed, with the STOP exchange or when its Test Interval ran out
49Ours, JSON only, the last warning code, where OB-UDPST won’t add one: the Test completed without a valid Maximum IP-Layer Capacity (no Sub-Interval met the Performance Criterion); it exits 0
2-6The Test completed with a warning (JSON only; it exits 0), in order: 4 nothing received for 1 s at some point (WARN_LOC_STOPPED), 5 the peer said it had stopped receiving (WARN_REM_STOPPED), 3 the peer missed Status PDUs (WARN_REM_STATUS), 2 Status PDUs never arrived (WARN_LOC_STATUS), 6 invalid PDUs (WARN_RX_INVPDU); the first of these that applies
50The configuration or command line is invalid
51The key file can’t be read or is invalid
75The Client or Server couldn’t start
100 + codeThe Server refused the Setup Request with that cmdResponse; 100 alone for a code RFC 9946 doesn’t define, 107 or 108 for a Setup Response that failed verification or the time window
150 + codeThe same for the Activation exchange: 152 for BADPARAM, 150 for an undefined code, 157 or 158 for a response that failed verification
200The Test didn’t complete: no answer from the Server in time, or the Watchdog ended it (ERROR_CONN_MIN, as OB-UDPST ends up reporting both)
201Ours: the Server coerced a parameter the Client was told to refuse
202Ours: the Server set a parameter the Client can’t run with
203Ours: the Servers gave a Multi-Connection Test’s Test Connections different parameters
204Ours: the Client user interrupted the running Test (Ctrl-C, or a signal to stop), or a write to its standard output failed, while the Test ran or with its final results. On Unix a Client stopped by a signal then dies of it, so a shell sees 128 plus the signal (130 for SIGINT, 143 for SIGTERM, 129 for SIGHUP); 204 is then the JSON ErrorStatus, unless every Test Connection had already ended, when it reflects the Test. On Windows, and for a failed write, the Client exits 204
255Any other error

A Multi-Connection Test reports as OB-UDPST does: once any Test Connection completed, success with the most severe warning of all; if none did, the first Test Connection’s error, by mcIndex, or 200 when none had one of its own (ERROR_CONN_MIN, which keeps an earlier error).

Our own codes sit in OB-UDPST’s connection-error range (200 plus an offset), which uses only 200, away from the ranges that add an IANA registered cmdResponse. Codes above 128 read like a shell’s “killed by signal” status; that comes with matching OB-UDPST. OB-UDPST keeps the last warning and lets a warning replace an error; we keep the most severe.

Conformance Records: exit-zero-with-warnings, exit-own-error-statuses, exit-most-severe-warning, client-interrupt.

Key files

Both the Client and the Server read Shared Keys from a key file, never from the command line. A file whose name ends in .toml is a TOML Key Table. Any other file is read as an OB-UDPST udpst.keys file.

TOML Key Tables

A TOML Key Table has one [[key]] row per Shared Key:

[[key]]
admin_key_name = "customers-2026"
key_id = 1
key_hex = "5b0c0e8d6f7a41c2b9d3e4f5061728394a5b6c7d8e9fa0b1c2d3e4f506172839"
accept_lifetime_end = "20270101000000Z"
send_lifetime_end = "20270101000000Z"
tests_per_quota_period = 12
FieldMeaning
admin_key_nameA name for the row, unique in the file. Errors name the row by it, never by the key
key_idThe keyId, 0 to 255, that Clients send
key_hex or key_textThe key, as hexadecimal digits or as UTF-8 text. Exactly one
send_lifetime_start, send_lifetime_end, accept_lifetime_start, accept_lifetime_endOptional, YYYYMMDDHHMMSSZ in UTC
kdf, alg_idOptional. HMAC-SHA-256 if present
operator, max_test_intervalAn Operator Key, and its longest Test Interval in seconds (60 or more). See Operator features
tests_per_quota_period, bytes_per_quota_periodThis row’s Quotas, which an Operator Key can’t have. See Hosting a Server

Unknown fields, empty keys and malformed values are errors. udpstp sets no minimum key length, but use at least 32 random bytes.

A file may start with format = 1, the format described here, which is also what a file without it has. A valid file keeps its meaning in later versions of udpstp: they may add columns, but never change what one means. A column this version doesn’t know, or a higher format, is refused rather than half read, so a file written for a newer udpstp fails to load instead of losing a restriction (Stable interfaces).

OB-UDPST key files

An OB-UDPST file has one <keyId>,<key> line per key, with keys of up to 64 bytes:

1,a-long-random-secret-for-consumers

Its rows have no lifetimes, Operator Keys or Quotas, so they are always valid.

Lifetimes and rotation

  • The Server accepts a keyId only through a row whose accept lifetime includes the Server’s clock.
  • The Client sends with the valid row whose send lifetime started most recently. If several rows tie, the Client refuses to guess, and --key-id must pick one.
  • Two rows may share a keyId only if neither their send nor their accept lifetimes overlap. Otherwise the Key Table is refused at startup.
  • A Test Connection keeps its keys when its row expires during a Test.
  • The Server reads the file once at startup. Add rows in advance, with future lifetimes, to rotate keys without a restart. Give the new key a new keyId whose lifetimes overlap the old one’s, then retire the old keyId.

Permissions

A key file must be private, or udpstp refuses it with exit status 51 before reading any key.

  • Linux and macOS: a regular file of mode 0600 or stricter, owned by the account running udpstp or by root.
  • Windows: a local NTFS file that only the running account, SYSTEM and Administrators can access. udpstp on Windows shows how to set this up.

On every platform the file must be at most 1 MiB. Keep it in a folder only its owner can write.

Recorded choices

  • lifetimes are written YYYYMMDDHHMMSSZ (key-lifetime-format).
  • key_text is an extension, and key_hex accepts either case (key-text-extension).
  • the KDF and AlgID columns are HMAC-SHA-256, and the Operator marker and max_test_interval extend RFC 7210 (key-table-alg-id, key-table-operator-extension).
  • OB-UDPST files are read like OB-UDPST reads them, with two exceptions (obudpst-key-file-reading).
  • a key file group or others can read or write is refused on Unix, and Windows policy mismatches or unverified permissions are refused (ADR 0015; conformance: key-file-private).
  • on Unix a key file must be owned by the user running udpstp or root (key-file-owner).
  • a key file over 1 MiB is refused (key-file-size-limit).

Platform differences

udpstp runs on Linux, macOS and Windows. A few features depend on what the operating system offers. Installing udpstp lists the release binaries.

FeatureLinuxmacOSWindows
Don’t Fragment (DF) on Test trafficSetSetNot yet: both ends warn
A packet too large for the pathLost, and reported as the Path MTULostFragmented, since DF isn’t set
DSCP markingYesYesNo: the Client asks for DSCP 0, and the Server changes every Test’s DSCP to 0, each with a warning
Key file checkMode 0600 or stricter, owned by the account or rootAs LinuxOnly the account, SYSTEM and Administrators may have access (details)
The Server’s Data Phase EngineA pool of Engine Threads, one per CPU it may use, each serving many Test ConnectionsOne Engine Thread per Test ConnectionOne Engine Thread per Test Connection
Fast pathsBatched sending (sendmmsg), UDP GSO, batched receiving (recvmmsg), kernel receive timestamps, waking on arrivalNoneNone
Receive Buffer Overflows countedYesNoNo
Socket buffer limit to raisenet.core.rmem_max, net.core.wmem_maxkern.ipc.maxsockbuf—
An interrupted Client exits withThe signal’s status, such as 130 for Ctrl-CThe signal’s status204
Unattended Serversystemd unit, container imageYour own service managerWindows service
Firewall for a ServerA stateful firewall needs only the Standard PortNot testedWindows Defender Firewall needs the whole Ephemeral Port range

Not yet measured

The Server’s pool of Engine Threads has been measured carrying 150 Test Connections of 4 Mbit/s each on one CPU core. These haven’t been measured yet:

  • 1,000,000 Load PDUs a second on one Server, or 10 Gbit/s in all;
  • Test Connections of 500 Mbit/s or more each sharing an Engine Thread. Give a Server that carries high-rate Multi-Connection Tests at least as many CPUs as such a Test has Test Connections.

Glossary

Press Ctrl+F (Command+F on macOS) to find a term on this page.

The terms udpstp uses, in its code, output, docs and issues, from the repository’s GLOSSARY.md. Each term has its own anchor, so it can be linked. Avoid lists words udpstp doesn’t use for it.

Roles

Client

The endpoint that starts every Test by sending a Setup Request to the Server’s Standard Port.

Avoid: Initiator, controller

Server

The endpoint that listens on the Standard Port, admits Test Connections, and runs the Load Rate Adjustment Algorithm in both directions of a Test.

Avoid: Responder, reflector

Load Sender / Load Receiver

The end of a Test Connection that sends Load PDUs, and the end that receives them and sends Status PDUs back. The Test’s direction decides which the Client plays.

Avoid: Src/Dst (RFC 9097 wording), sender/receiver unqualified

Client user

Whoever runs a Client and chooses its Test.

Avoid: User (unqualified)

Server administrator

Whoever runs a Server and its Key Table, and so decides which Shared Keys it accepts and which are Operator Keys (RFC 9946 §6.2.1’s server administrator).

Avoid: operator (lowercase)

Operator

A network operator measuring within its own domain, the only party RFC 9946 §4.1 lets run Fixed-Rate Tests. A Server knows one only by an Operator Key.

Avoid: Admin, privileged user

Consumer

Anyone measuring their own access (RFC 9946 §3); to a Server, any Client without an Operator Key, an unauthenticated one included.

Avoid: Subscriber (unqualified)

Tests

Upstream Test

A Test where the Client sends Load PDUs and the Server sends Status PDUs.

Avoid: Upload test, uplink test

Downstream Test

A Test where the Server sends Load PDUs and the Client sends Status PDUs.

Avoid: Download test, downlink test

Test

One measurement a Client runs in one direction: one or more Test Connections, to one or more Servers, sharing one mcIdent. A Server sees as the Test only the Test Connections that reach it from one Client address with that mcIdent.

Avoid: Speed test (unqualified), run, session

mcIdent

The non-zero pseudorandom identifier every Setup Request of one Test carries, which associates its Test Connections (RFC 9946 §6.1). Beside it, mcIndex numbers the Test Connections from 0 and mcCount says how many the Client attempts.

Avoid: Test ID, session ID

Test Connection

One Client–Server exchange of a Test, numbered by mcIndex, from its Setup Request to its end; once admitted it has its own Ephemeral Port.

Avoid: Session, flow, connection unqualified

Multi-Connection Test

A Test with more than one Test Connection, told apart by mcIndex. All its Test Connections share the Test’s direction, and the Client aggregates their results.

Avoid: Multi-flow test, parallel test

Minimum Test Connections

The fewest of a Test’s Test Connections that must be set up and keep running for the Test to go on; all of them unless the Client user sets fewer.

Avoid: Min connections, quorum

Multi-Connection Coordinator

The part of the Client that runs a Test’s Test Connections as one Test: the client process’s job in RFC 9946 §4.

Avoid: Test manager, connection pool

Search Test

A Test whose sending rate the Load Rate Adjustment Algorithm sets, starting at the default row or a requested one.

Avoid: Adaptive test

Fixed-Rate Test

A Test whose Test Connections each hold one Sending Rate Table row for the whole Test Interval instead of running the Load Rate Adjustment Algorithm.

Avoid: Static test, constant-rate test

The Server and its limits

Standard Port

The Server UDP port where Setup Requests arrive: 24601, RFC 9946’s assigned port, unless the Server is set to another.

Avoid: Control port (RFC 9946’s name for it), listen port

Ephemeral Port

The Server UDP port allocated to one Test Connection and returned as testPort in the Setup Response; all later PDUs of the Test Connection use it.

Avoid: Test port, data port

Source

Where the Server counts Tests from: the Client’s IPv4 address, or the /64 its IPv6 address is in. It stands in for the RFC’s “path”, which the Server can’t see.

Avoid: Client address (for this grouping), path (for this grouping)

maxBandwidth

The rate, in Mbps, a Setup Request says its Test Connection expects to send or receive in its direction (RFC 9946 §6.1). The Server admits it against the Admitted Rate Cap and never searches above it.

Avoid: Bandwidth (unqualified), requested capacity

Admitted Rate Cap

The most maxBandwidth the Server admits at once in one direction, summed over the Test Connections of its activated Tests.

Avoid: Bandwidth cap, capacity limit

Unactivated Test Connection

A Test Connection the Server has admitted but whose Activation Request it hasn’t yet accepted.

Avoid: Pending connection, half-open connection

Admission

The Server’s acceptance of a Setup Request within its limits, which gives the Test Connection an Ephemeral Port and a place among the Test Connections the Server serves until it ends.

Setup Request budget

The Server’s limit on how fast it takes Setup Requests: one global budget for those not yet verified, and one per Source for authentic ones.

Quota

A Server’s limit on how many Tests, and how many load bytes, one Source or one Key Table row may run per Quota Period.

Avoid: Rate limit (that is a Setup Request budget), cap (that limits what the Server admits, not what a Source uses over time)

Quota Period

The period a Quota is counted over; a Quota refills evenly over it rather than all at once when it ends.

Packets on the path

DF

The IPv4 Don’t Fragment bit, set on test traffic so it is lost rather than fragmented on a path with a smaller MTU (RFC 9946 §5); on IPv6, where routers never fragment, the sender doesn’t fragment either.

Avoid: No-fragment flag

DSCP

The Diffserv code point test traffic is marked with, which the Client asks for in dscpEcn and the Server allows or coerces to 0 (RFC 9946 §7.1). ECN, the rest of that octet, is always Not-ECT.

Avoid: TOS (for the DSCP alone), priority, packet class

MaxHops

The IPv4 TTL and IPv6 hop limit test traffic is sent with, set at each end from the expected path length so the traffic can’t stray far beyond the intended path (RFC 9946 §10).

Avoid: TTL (for the setting), hop count

Path MTU

The largest IP packet every link between the Client and the Server carries (RFC 9097 §6.5’s PMTU); with DF set, a larger one is lost.

Avoid: MTU (unqualified; see Server MTU)

Server MTU

The largest IP packet the Server is configured to send. Rows of its Sending Rate Table above 1 Gbps keep their rates with more, smaller packets when their own would be larger.

Avoid: Packet size limit, frame size

Jumbo / Traditional MTU

The two Setup Request modifiers that choose the Sending Rate Table’s packet sizes (RFC 9946 §6.1). Rates up to 1 Gbps use IP packets of at most 1250 bytes, or 1500 with Traditional MTU; Jumbo, set by default, lets faster rates use larger packets, up to 9000 bytes.

Phases and messages

Control Phase

The Setup exchange and the Activation exchange that precede measurement.

Avoid: Handshake, negotiation

Data Phase

The exchange of Load PDUs and Status PDUs, ending with the STOP exchange or with an early end such as when the Watchdog expires.

Avoid: Test phase, measurement phase

PDU

A single UDPSTP message carried in one UDP datagram, identified by its pduId.

Avoid: Packet, frame (message is an accepted synonym in the RFC)

Control PDU

A PDU of the Control Phase: a Test Setup, Null or Test Activation PDU.

Message Verification Procedure

The checks every received Control PDU and Status PDU must pass, in order, before it is acted on: size, checksum when used, authMode, authDigest, then authUnixTime (RFC 9946 §6.2). A PDU failing one is invalid.

Setup exchange

The Setup Request, the Setup Response and, from the Ephemeral Port, the Null Request.

Setup Request / Setup Response

The Test Setup PDU (0xACE1) sent by the Client, and the same PDU returned by the Server with its Command Response and the Ephemeral Port.

Null Request

The Null PDU (0xDEAD) the Server sends from the new Ephemeral Port to open its own firewall pinhole; it gets no reply.

Activation exchange

The Activation Request and the Activation Response.

Activation Request / Activation Response

The Test Activation PDU (0xACE2) carrying the Test parameters, and the Server’s accepted, coerced, or rejected version of it.

Command Response

The cmdResponse code in a Setup Response or Activation Response: ACKOK, or a code naming why the request was rejected.

Avoid: Error code, status code

Silent Rejection

Refusing a request by sending nothing back (RFC 9946 §5.3.1). A request only ignored, such as a duplicate already answered, is not one.

Avoid: Drop (unqualified)

Coercion

The Server’s change of one Activation Request parameter to a value it accepts, returned in the Activation Response with ACKOK.

Avoid: Override, adjustment (of a parameter)

Load PDU

The PDU (0xBEEF) that carries the measurement load, with a sequence-numbered header followed by filler payload.

Avoid: Data packet, test packet

Status PDU

The PDU (0xFEED) the Load Receiver sends every Trial Interval, carrying reception statistics and, when the Server sends it, the next sending parameters.

Avoid: Feedback message, status feedback message, report

Stale Status PDU

A Status PDU whose spduSeqNo is 0 or no greater than that of the last one processed: a duplicated, reordered or replayed one.

STOP exchange

The graceful end of a Test Connection: the Server marks its outgoing PDUs TEST_ACT_STOP2 and the Client confirms by marking its own.

Avoid: STOP2 exchange, stop handshake, teardown

Timers

Load PDU timeout / Status PDU timeout

The Load Receiver’s timer, reset by every Load PDU received, and the Load Sender’s, reset by every Status PDU received, whose expiry stops that end sending (RFC 9946 §5.1). The Test Connection ends later, at the Watchdog.

Watchdog

The timer at each end of a Test Connection, reset by every valid PDU received while that end still sends, whose expiry ends the Test Connection without the STOP exchange (ADR 0002).

Avoid: Keepalive, idle timer, test termination timeout (RFC 9946 §6.1’s name for it)

Test initiation timer

The Client’s timer, started with its Setup Request, that ends a Control Phase which hasn’t reached the Data Phase within 3 s (RFC 9946 §6.1 lets it reuse the “test termination timeout”), or sooner if the Client user says so.

Activation Watchdog

The Server’s timer, started with a Setup Response, that closes the Test Connection if no valid Activation Request arrives within 3 s.

Rate control

Load Rate Adjustment Algorithm

The Server-side logic that picks the next Sending Rate Table row from measured impairments. The registered ones are algorithm B (ITU-T Y.1540 Amd2) and algorithm C (BBF TR-471).

Avoid: Congestion control, CCA, search algorithm

Sending Rate Table

The Server’s ordered table of rows, numbered from 0, each holding the transmission parameters for one offered sending rate.

Avoid: Rate table, rate ladder

Sending rate

The rate of the Sending Rate Table row a Load Sender is set to send at (RFC 9946 §8.1; RFC 9097’s “offered load”).

Avoid: Offered load

Sending Rate Structure

The transmission parameters of one Sending Rate Table row as carried on the wire (srStruct: two interval/payload/burst sets plus an add-on).

Avoid: Rate row (on the wire)

Sequence Error

A Load PDU the Load Receiver counts, from the lpduSeqNo it sees, as lost, out of order or duplicated (RFC 9946 §8.2’s seqErrLoss, seqErrOoo and seqErrDup). With ignoreOooDup set, as by default, only the lost ones count against seqErrThresh (RFC 9946 §7.1).

Avoid: Sequence anomaly

Clean Interval

A Trial Interval whose counted Sequence Errors are at or below the threshold and whose delay variation is below the lower threshold. A Trial Interval with no delay variation sample is never clean.

Errored Interval

A Trial Interval whose counted Sequence Errors exceed the threshold or whose delay variation exceeds the upper threshold.

Avoid: Impaired interval, congested interval

Hold Interval

A Trial Interval that is neither clean nor errored, on which the algorithm makes no change.

Avoid: Neither, no-change interval

Congestion

What the algorithm infers when a configured number of back-to-back Errored Intervals occur; inferring it sets Congestion Confirmed.

Avoid: Overload, impairment (an impairment is a single-interval observation)

Congestion Confirmed

The latched state entered when Congestion is inferred. Algorithm B stays in it, and so in Single-Step Mode, for the rest of the Test Connection; algorithm C clears it only by a Fast-Mode Retry.

Avoid: Congested state, congestion flag

Fast Mode

The state of a Load Rate Adjustment Algorithm, below the 1 Gbps row and before Congestion Confirmed, in which the rate rises by more than one row at a time.

Avoid: Ramp-up, slow start, high-speed mode (the RFC’s name, kept only in the highSpeedDelta field)

Single-Step Mode

The state in which every rate change is exactly one Sending Rate Table row up or down.

Avoid: Slow mode, fine-tuning

Fast-Mode Retry

Algorithm C’s re-entry into Fast Mode after enough single-step moves, clearing Congestion Confirmed.

Avoid: Retry (unqualified; clashes with retransmission)

Lost Status Backoff

A Load Sender’s lowering of its sending rate in a Search Test, as for an Errored Interval, each time no Status PDU has arrived for RFC 9097 §8.1’s Lost Status Backoff timeout, which grows by one Trial Interval with each backoff.

Avoid: Feedback timeout backoff

Over-rate policing

The Server’s ending of a Test Connection whose Load Sender keeps sending well above the sending rates the Server assigned it (ADR 0008, ADR 0011).

Measurement intervals and results

Test Interval

The whole measurement duration (testIntTime, seconds) after which the Server starts the STOP exchange.

Avoid: Test duration, run time

Sub-Interval

A fixed slice of the Test Interval (subIntPeriod, ms, a whole number of Trial Intervals) over which statistics are saved and reported in sisSav, and over which each IP-Layer Capacity is measured.

IP-Layer Capacity

The IP-layer bit rate received in one Sub-Interval, IP and UDP headers included, over its nominal length (RFC 9097 §5.3, C(T,dt,PM)).

Avoid: Sub-Interval throughput, rate (unqualified)

Trial Interval

The period between consecutive Status PDUs (trialInt, ms); the unit the algorithm decides on.

Avoid: Feedback interval, status interval

Loss Ratio

The share of the Load PDUs sent in a Sub-Interval that were lost (RFC 9097 §5.4’s ratio of lost packets, which §9 reports at the maximum).

Avoid: Packet loss rate, drop rate

Tmax

The longest a Load PDU may take to arrive and still count as received rather than lost (RFC 9097 §4).

Avoid: Loss timeout, waiting time

Performance Criterion

The loss target a Sub-Interval must meet for its IP-Layer Capacity to count towards the Maximum IP-Layer Capacity (RFC 9097 §6.3’s PM criterion): at most a set Loss Ratio of the Load PDUs sent in it, or one lost Load PDU per Test Connection. One in which none were received meets none.

Avoid: PM threshold, quality filter

Maximum IP-Layer Capacity

The highest IP-Layer Capacity of the Sub-Intervals that meet the Performance Criterion, within one Capacity Mode (RFC 9097 §6.3). The Test’s headline result; a Test where none meets it has none.

Avoid: Throughput, bandwidth (except in maxBandwidth), speed, goodput

Highest IP-Layer Capacity

The highest IP-Layer Capacity of any Sub-Interval, whether or not it meets the Performance Criterion: what OB-UDPST reports as its maximum.

Avoid: Peak rate, raw maximum

Capacity Mode

One of the consecutive stretches of Sub-Intervals, split where the Client user says, that each get their own Maximum IP-Layer Capacity, for a path whose capacity changes on a schedule such as a “turbo mode” (RFC 9097 §6.6).

Avoid: Mode (unqualified), phase, first mode (TR-471’s name, kept only in –first-mode-sub-intervals and FirstModeSplitIgnored)

IP-Layer Sender Bit Rate

The IP-layer bit rate the Load Sender actually sent, over each st and each Sub-Interval’s worth of them (RFC 9097 §7’s B(S,st)); a Load Receiver can only estimate it, from the Load PDUs that arrive.

Avoid: Send rate (unqualified), Sender IP-Layer Bit Rate

st

The length of the intervals, counted from the Load Sender’s first send, over which the IP-Layer Sender Bit Rate is taken (RFC 9097 §7.2); udpstp’s is one Trial Interval, offset from the Load Receiver’s Trial Intervals by the one-way delay.

Security

Authentication Mode

How much of a Test Connection is authenticated (authMode): mode 1 authenticates the Control Phase and mode 2 also authenticates Status PDUs. RFC 9946 defines only these two, written mode 1 and mode 2.

Avoid: Auth level, security mode

Unauthenticated Mode

OB-UDPST’s authMode 0, which authenticates nothing and which RFC 9946 does not define; udpstp enables it only when both ends opt in, to interoperate with OB-UDPST.

Avoid: Mode 0, open mode

Key Table

The table of Shared Keys a Client or Server authenticates with, one row per key with RFC 7210’s fields, send and accept lifetimes among them (RFC 9946 §5.4).

Avoid: Key store, keyring

Shared Key

A long-lived secret in the Key Table, selected by keyId, that is only ever used as input to the KDF.

Avoid: Password, secret unqualified

Operator Key

A Shared Key that the Server’s Key Table marks as an Operator’s: only its Tests may be Fixed-Rate Tests, and they are spared some limits the Server sets for Consumers.

Avoid: Admin key, privileged key

Derived Keys

The Client Authentication Key and Server Authentication Key, derived once per Test Connection from the Shared Key and the first Setup Request’s authUnixTime.

Avoid: Session keys

Troubleshooting Mode

A Server setting that answers authentication failures with a Command Response instead of a Silent Rejection, for debugging Client setups.

Avoid: Debug mode, verbose mode

Conformance

Requirement

One normative statement of RFC 9946, or of RFC 9097 as RFC 9946 uses it, with a stable ID. It counts as met when a test proves it, unless it is recorded otherwise.

Deviation

A recorded departure from a Requirement, with its reason: a non-conformance, an interop accommodation, a platform gap or an unmet SHOULD.

Avoid: Deviation for a difference from OB-UDPST that conforms to the RFC (an OB-UDPST Difference)

Interpretation

A recorded reading of a Requirement that is ambiguous, silent on a case, or collides with another one, or a choice the RFC leaves to the implementation; udpstp still conforms.

OB-UDPST

The Broadband Forum’s open-source C implementation of UDPSTP, and udpstp’s interop peer.

OB-UDPST Difference

Behaviour in which udpstp and OB-UDPST differ while udpstp conforms to the RFC.

Avoid: Deviation (from OB-UDPST)

Conformance Record

The record of one Deviation, Interpretation or OB-UDPST Difference: what udpstp does, why, and the tests that prove it.

Avoid: Registry entry, deviations registry

Load schedule

Transmitter

One of a Load Sender’s two independent Burst schedules (RFC 9946 §7.1’s dual independent transmitters), each sending one Burst per interval as one interval/payload/burst set of its transmission parameters says.

Avoid: Timer, channel, stream

Burst

The Load PDUs one Transmitter sends back to back at one scheduled time: burstSize datagrams, plus the Add-on on Transmitter 2.

Avoid: Batch, train

Add-on

The single extra Load PDU of udpAddon2 bytes at the end of each Transmitter 2 Burst.

Send Plan

The datagrams due at one scheduled time: the Deferred Datagrams still waiting, then one new Burst unless it is withheld.

Avoid: Burst plan

Deferred Datagram

A Load PDU of a Send Plan that the host could not send. It leads the next Send Plan with its original sequence number and a fresh lpduTime.

Avoid: Retransmission, retry

Catch-up budget

How late a missed Burst may be and still be sent when the Load Sender catches up (ADR 0012).

Avoid: Catch-up window, grace period

Late Burst

A Burst sent after its scheduled time but less late than the catch-up budget, or the most recent Burst due.

Burst lateness

How long after a Burst’s scheduled time the Load Sender writes its Send Plan, and so stamps its lpduTime; the time from lpduTime to the datagram’s departure is the send lag.

Avoid: Burst delay

Skipped Burst

A Burst not sent because it was at least the catch-up budget late when its turn came and wasn’t the most recent Burst due; its Load PDUs take no lpduSeqNo and are missing from the IP-Layer Sender Bit Rate.

Avoid: Dropped burst, lost burst (loss is what a Load Receiver counts from lpduSeqNo gaps)

Withheld Burst

A Burst not sent because at least as many Deferred Datagrams as it holds are still waiting; counted like a Skipped Burst.

Host

Receive Buffer Overflow

A datagram the host’s OS discarded at a Test Connection’s socket before the Data Phase Engine read it, mostly for a full receive buffer; a Load Receiver counts it as lost though the path didn’t lose it.

Avoid: Dropped datagram, socket drop, RcvbufErrors (the kernel’s host-wide counter)

Test Time Base

The steady clock every Data Phase time is taken from, which changes to the wall clock never move; lpduTime and spduTime are this clock plus a wall-clock offset fixed when the Test Connection activates.

Avoid: System time, wall time (for Data Phase timing)

Data Phase Engine

The part of an endpoint that sends each Test Connection’s Send Plans at their scheduled times and receives its Load PDUs and Status PDUs.

Avoid: Runtime (unqualified)

Engine Thread

A thread that runs the Data Phase Engine for one or more Test Connections.

Avoid: Engine worker, per-core thread

Conformance

Every normative statement of RFC 9946, and of the RFC 9097 sections it uses, is a Requirement with a stable ID: 420 in all. A Requirement counts as met when a test that runs on Linux proves it, unless it is recorded otherwise. These pages are generated by cargo xtask req’s checks when the site is built.

348 of 420 Requirements are proven by tests. Of the rest, 24 are Deviations, each recorded with its reason, 33 are operational (they concern how udpstp is deployed or used rather than its code), and 15 don’t apply to udpstp.

Coverage by section

Conformance Records

Where udpstp departs from a Requirement, reads an ambiguous one, or differs from OB-UDPST, a Conformance Record says what it does, why, and which tests prove it.

How the records work describes the files these pages come from.

States

  • Tested: a test that runs on Linux, in CI and before every push, proves it.
  • Tested nightly: only tests in the nightly workflow prove it.
  • Pending: no test proves it yet.
  • Deviation: udpstp departs from it, as a Deviation records.
  • Interpretation: udpstp meets it as an Interpretation reads it.
  • Operational: it concerns deployment or use, not udpstp’s code.
  • Not applicable: it doesn’t apply to udpstp.

RFC 9097 §2: Scope, Goals, and Applicability

The 6 Requirements of RFC 9097 §2, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested3
Operational3

RFC9097-2-1

Operational

It is RECOMMENDED to discontinue non-measurement traffic that shares a subscriber’s dedicated resources while testing: measurements may not be accurate, and throughput of competing elastic traffic may be greatly reduced.

Note: Advice to the user running a Test.

RFC9097-2-2

Operational

It MUST only be used in the application of diagnostic and operations measurements as described in this memo.

Note: “It” is the Load Rate Adjustment Algorithm; RFC 9946 section 5.1 item 2 says the same.

RFC9097-2-3

Operational

It MUST only be used in circumstances consistent with Section 10 (“Security Considerations”).

Note: RFC 9946 section 5.1 item 11 says the same.

RFC9097-2-4

Tested

If a network operator is certain of the IP-Layer Capacity to be validated, then testing MAY start with a fixed-rate test at the IP-Layer Capacity and avoid activating the load adjustment algorithm.

Note: Fixed-Rate Test, Operator Key only; RFC 9946 section 5.1 item 10.

Proving tests:

RFC9097-2-5

Tested

However, the stimulus for a diagnostic test (such as a subscriber request) strongly implies that there is no certainty, and the load adjustment algorithm is RECOMMENDED.

Note: A Search Test is the default.

Proving tests:

RFC9097-2-6

Tested

Finally, the measurement system’s load rate adjustment algorithm SHALL NOT be provided with the exact capacity value to be validated a priori.

Note: A Search Test starts at the default row or a requested srIndexConf row, never at a known capacity.

Proving tests:

RFC 9097 §4: General Parameters and Definitions

The 9 Requirements of RFC 9097 §4, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested7
Not applicable2

RFC9097-4-1

Not applicable

This section lists the REQUIRED input factors to specify a Sender or Receiver metric.

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9097-4-L1

Tested

The nominal duration of a measurement interval at the Destination (default 10 sec).

Note: I, the Test Interval (testIntTime).

Proving tests:

RFC9097-4-L2

Tested

The nominal duration of m equal sub-intervals in I at the Destination (default 1 sec).

Note: dt, the Sub-Interval (subIntPeriod).

Proving tests:

RFC9097-4-L3

Tested

The results are evaluated throughout the test to determine how to adjust the current offered load rate at the Sender (default 50 msec).

Note: FT, the Trial Interval (trialInt).

Proving tests:

RFC9097-4-L4

Tested

A maximum waiting time for test packets to arrive at the Destination, set sufficiently long to disambiguate packets with long delays from packets that are discarded (lost), such that the distribution of one-way delay is not truncated.

Note: Tmax.

Conformance Records: tmax-within-the-sub-interval.

Proving tests:

RFC9097-4-L5

Tested

The number of different flows synthesized by the method (default one flow).

Note: F; more than one is a Multi-Connection Test.

Proving tests:

RFC9097-4-2

Not applicable

Note: The IPv6 flow label SHOULD be included in the flow definition when routers have complied with the guidelines provided in [RFC6438].

Note: About the flow definition: udpstp sets no IPv6 flow label, and the OS’s is constant for a socket, so a Test Connection is one flow with or without it.

RFC9097-4-3

Tested

If there is payload compression in the path and tests intend to characterize a possible advantage due to compression, then payload content SHOULD be supplied by a pseudorandom sequence generator, by using part of a compressed file, or by other means.

Note: Random payload (CHTA_RAND_PAYLOAD, --random-payload).

Proving tests:

RFC9097-4-4

Tested

At least one fundamental metric and target performance threshold MUST be supplied (such as one-way IP packet loss [RFC7680] equal to zero).

Note: PM: the Performance Criterion.

Proving tests:

RFC 9097 §5: IP-Layer Capacity Singleton Metric Definitions

The 17 Requirements of RFC 9097 §5, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested13
Deviation1
Operational1
Not applicable2

RFC9097-5.2-1

Not applicable

This section lists the REQUIRED input factors to specify the metric, beyond those listed in Section 4.

Note: The section adds no Parameters.

RFC9097-5.3-1

Not applicable

This section defines the REQUIRED aspects of the measurable IP-Layer Capacity Metric (unless otherwise indicated) for measurements between specified Source and Destination hosts:

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9097-5.3-L1

Deviation

Define the IP-Layer Capacity, C(T,dt,PM), to be the number of IP- Layer bits (including header and data fields) in packets that can be transmitted from the Src host and correctly received by the Dst host during one contiguous sub-interval, dt in length.

Note: IP-Layer Capacity; a definition, but the one the Sub-Interval results compute.

Recorded in approximate-upstream-aggregation.

Conformance Records: duplicates-count-as-received, multi-connection-common-grid.

Proving tests:

RFC9097-5.3-2

Tested

Anticipating a Sample of Singletons, the number of sub-intervals with duration dt MUST be set to a natural number m, so that T+I = T + m*dt with dtn+1 - dtn = dt for 1 <= n <= m.

Note: testIntTime / subIntPeriod Sub-Intervals.

Conformance Records: sub-interval-whole-trial-intervals.

Proving tests:

RFC9097-5.3-3

Tested

Parameter PM represents other performance metrics (see Section 5.4 below); their measurement results SHALL be collected during measurement of IP-Layer Capacity and associated with the corresponding dtn for further evaluation and reporting.

Note: Per-Sub-Interval statistics (sisSav).

Proving tests:

RFC9097-5.3-4

Tested

Users SHALL specify the Parameter Tmax as required by each metric’s reference definition.

Note: Tmax.

Conformance Records: tmax-within-the-sub-interval.

Proving tests:

RFC9097-5.3-5

Tested

PM represents other performance metrics (see Section 5.4 below); their measurement results SHALL be collected during measurement of IP-Layer Capacity and associated with the corresponding dtn for further evaluation and reporting.

Note: Bullet restating RFC9097-5.3-3.

Proving tests:

RFC9097-5.3-6

Tested

All sub-intervals MUST be of equal duration.

Note: subIntPeriod is a whole number of Trial Intervals.

Conformance Records: sub-interval-whole-trial-intervals.

Proving tests:

RFC9097-5.3-7

Operational

The bit rate of the physical interface of the measurement devices MUST be higher than the smallest of the links on the path whose C(T,I,PM) is to be measured (the bottleneck link).

Note: Deployment of the measurement hosts.

RFC9097-5.3-8

Tested

Measurements according to this definition SHALL use the UDP transport layer.

Proving tests:

RFC9097-5.3-9

Tested

The measurement SHOULD use a randomized Source port or equivalent technique, and SHOULD send responses from the Source address matching the test packet Destination address.

Note: The Client’s ports are assigned by the OS from its ephemeral range, not udpstp’s Ephemeral Ports (the Server’s); replies leave from the arrival address (ADR 0007).

Proving tests:

RFC9097-5.4-1

Tested

The statistics used to summarize RTD[dtn,dtn+1] MAY include the minimum, maximum, median, mean, and the range = (maximum - minimum).

Proving tests:

RFC9097-5.4-2

Tested

The statistics used to summarize OWL[dtn,dtn+1] MAY include the count of lost packets and the ratio of lost packets.

Note: Loss Ratio.

Proving tests:

RFC9097-5.4-3

Tested

Other metrics MAY be measured: one-way reordering, duplication, and delay variation.

Proving tests:

RFC9097-5.6-1

Tested

The IP-Layer Capacity SHOULD be reported with at least single-Megabit resolution, in units of Megabits per second (Mbps) (which, to avoid any confusion, is 1,000,000 bits per second).

Proving tests:

RFC9097-5.6-2

Tested

The related One-Way Loss metric and Round-Trip Delay measurements for the same Singleton SHALL be reported, also with meaningful resolution for the values measured.

Proving tests:

RFC9097-5.6-3

Tested

Individual Capacity measurements MAY be reported in a manner consistent with the Maximum IP-Layer Capacity; see Section 9.

Proving tests:

RFC 9097 §6: Maximum IP-Layer Capacity Metric Definitions (Statistics)

The 18 Requirements of RFC 9097 §6, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested12
Operational3
Not applicable3

RFC9097-6.2-1

Not applicable

This section lists the REQUIRED input factors to specify the metric, beyond those listed in Section 4.

Note: The section adds no Parameters.

RFC9097-6.3-1

Not applicable

This section defines the REQUIRED aspects of the Maximum IP-Layer Capacity Metric (unless otherwise indicated) for measurements between specified Source and Destination hosts:

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9097-6.3-L1

Tested

Define the Maximum IP-Layer Capacity, Maximum_C(T,I,PM), to be the maximum number of IP-Layer bits n0[dtn,dtn+1] divided by dt that can be transmitted in packets from the Src host and correctly received by the Dst host, over all dt-length intervals in [T,T+I] and meeting the PM criteria.

Note: Maximum IP-Layer Capacity; a definition, but the one the results compute.

Conformance Records: shallow-queue-search-alternates-at-the-bottleneck, duplicates-count-as-received, empty-sub-interval-meets-no-criterion, json-at-max-is-the-maximum.

Proving tests:

RFC9097-6.3-2

Tested

The number of sub-intervals with duration dt MUST be set to a natural number m, so that T+I = T + m*dt with dtn+1 - dtn = dt for 1 <= n <= m.

Note: testIntTime / subIntPeriod Sub-Intervals.

Conformance Records: sub-interval-whole-trial-intervals.

Proving tests:

RFC9097-6.3-3

Tested

At least one target performance threshold (PM criterion) MUST be defined.

Note: The Performance Criterion.

Conformance Records: shallow-queue-search-alternates-at-the-bottleneck, loss-allowance-per-test-connection.

Proving tests:

RFC9097-6.3-4

Not applicable

If more than one metric and target performance threshold is defined, then the sub-interval with the maximum number of bits transmitted MUST meet all the target performance thresholds.

Note: udpstp defines one Performance Criterion, the Loss Ratio, so there is never more than one threshold to meet.

Conformance Records: shallow-queue-search-alternates-at-the-bottleneck.

RFC9097-6.3-5

Tested

Users SHALL specify the Parameter Tmax as required by each metric’s reference definition.

Note: Tmax.

Conformance Records: tmax-within-the-sub-interval.

Proving tests:

RFC9097-6.3-6

Tested

At least one target performance threshold (PM criterion) MUST be defined.

Note: Bullet restating RFC9097-6.3-3.

Proving tests:

RFC9097-6.3-7

Tested

All sub-intervals MUST be of equal duration.

Note: subIntPeriod is a whole number of Trial Intervals.

Conformance Records: sub-interval-whole-trial-intervals.

Proving tests:

RFC9097-6.3-8

Operational

The bit rate of the physical interface of the measurement systems MUST be higher than the smallest of the links on the path whose Maximum_C(T,I,PM) is to be measured (the bottleneck link).

Note: Deployment of the measurement hosts.

RFC9097-6.3-9

Tested

Measurements according to these definitions SHALL use the UDP transport layer.

Proving tests:

RFC9097-6.4-L1

Tested

The interval dtn,dtn+1 where Maximum_C(T,I,PM) occurs is the reporting sub-interval for RTD[dtn,dtn+1] and OWL[dtn,dtn+1] within RTD[T,I] and OWL[T,I].

Note: Loss and RTT are reported for the Sub-Interval of the maximum.

Proving tests:

RFC9097-6.4-1

Tested

Other metrics MAY be measured: one-way reordering, duplication, and delay variation.

Proving tests:

RFC9097-6.5-1

Operational

If traffic conditioning (e.g., shaping, policing) applies along a path for which Maximum_C(T,I,PM) is to be determined, different values for dt SHOULD be picked and measurements executed during multiple intervals [T,T+I].

Note: The user picks subIntPeriod per Test.

RFC9097-6.5-2

Operational

Each duration dt SHOULD be chosen so that it is an integer multiple of increasing values k times serialization delay of a Path MTU (PMTU) at the physical interface speed where traffic conditioning is expected.

Note: The user picks subIntPeriod per Test.

RFC9097-6.6-1

Tested

The IP-Layer Capacity SHOULD be reported with at least single-Megabit resolution, in units of Megabits per second (Mbps) (which, to avoid any confusion, is 1,000,000 bits per second).

Proving tests:

RFC9097-6.6-2

Tested

The related One-Way Loss metric and Round-Trip Delay measurements for the same Singleton SHALL be reported, also with meaningful resolution for the values measured.

Proving tests:

RFC9097-6.6-3

Tested

When there are demonstrated and repeatable Capacity modes in the Sample, the Maximum IP-Layer Capacity SHALL be reported for each mode, along with the relative time from the beginning of the stream that the mode was observed to be present.

Note: Capacity Modes.

Proving tests:

RFC 9097 §7: IP-Layer Sender Bit Rate Singleton Metric Definitions

The 10 Requirements of RFC 9097 §7, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested7
Deviation1
Not applicable2

RFC9097-7.2-1

Not applicable

This section lists the REQUIRED input factors to specify the metric, beyond those listed in Section 4.

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9097-7.2-L1

Tested

The nominal duration of N sub-intervals in S (default st = 0.05 seconds).

Note: st, one Trial Interval.

Proving tests:

RFC9097-7.2-2

Tested

S SHALL be longer than I, primarily to account for on-demand activation of the path, or any preamble to testing required, and the delay of the path.

Proving tests:

RFC9097-7.2-3

Tested

st SHOULD be much smaller than the sub-interval dt and on the same order as FT; otherwise, the rate measurement will include many rate adjustments and include more time smoothing, possibly smoothing the interval that contains the Maximum IP-Layer Capacity (and therefore losing relevance).

Proving tests:

RFC9097-7.3-1

Not applicable

This section defines the REQUIRED aspects of the IP-Layer Sender Bit Rate Metric (unless otherwise indicated) for measurements at the specified Source on packets addressed for the intended Destination host and matching the required Type-P:

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9097-7.3-2

Tested

Define the IP-Layer Sender Bit Rate, B(S,st), to be the number of IP- Layer bits (including header and data fields) that are transmitted from the Source with address pair Src and Dst during one contiguous sub-interval, st, during the test interval S (where S SHALL be longer than I) and where the fixed-size packet count during that single sub- interval st also provides the number of IP-Layer bits in any interval, [stn,stn+1].

Note: IP-Layer Sender Bit Rate.

Proving tests:

RFC9097-7.3-3

Tested

Measurements according to this definition SHALL use the UDP transport layer.

Proving tests:

RFC9097-7.3-4

Deviation

Any feedback from the Dst host to the Src host received by the Src host during an interval [stn,stn+1] SHOULD NOT result in an adaptation of the Src host traffic conditioning during this interval (rate adjustment occurs on st interval boundaries).

Recorded in rate-changes-when-a-status-pdu-arrives.

RFC9097-7.4-1

Tested

Both the Sender and Receiver (or Source and Destination) bit rates SHOULD be assessed as part of an IP-Layer Capacity measurement.

Proving tests:

RFC9097-7.5-1

Tested

The IP-Layer Sender Bit Rate SHALL be reported with meaningful resolution, in units of Megabits per second (which, to avoid any confusion, is 1,000,000 bits per second).

Proving tests:

RFC 9097 §8: Method of Measurement

The 49 Requirements of RFC 9097 §8, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested37
Deviation6
Operational6

RFC9097-8-1

Tested

It is REQUIRED per the architecture of the method that two cooperating hosts operate in the roles of Src (test packet Sender) and Dst (Receiver) with a measured path and return path between them.

Note: Load Sender and Load Receiver.

Proving tests:

RFC9097-8-2

Tested

The duration of a test, Parameter I, MUST be constrained in a production network, since this is an active test method and it will likely cause congestion on the path from the Src host to the Dst host during a test.

Note: testIntTime’s maximum.

Proving tests:

RFC9097-8.1-1

Operational

The algorithm described in this section MUST NOT be used as a general Congestion Control Algorithm (CCA).

Note: RFC 9946 section 5.1 item 1 says the same.

RFC9097-8.1-2

Tested

A table SHALL be pre-built (by the test administrator), defining all the offered load rates that will be supported (R1 through Rn, in ascending order, corresponding to indexed rows in the table).

Note: Sending Rate Table.

Proving tests:

RFC9097-8.1-3

Deviation

It is RECOMMENDED that rates begin with 0.5 Mbps at index zero, use 1 Mbps at index one, and then continue in 1 Mbps increments to 1 Gbps.

Note: Sending Rate Table.

Recorded in row-zero-below-half-a-megabit.

RFC9097-8.1-4

Deviation

Above 1 Gbps, and up to 10 Gbps, it is RECOMMENDED that 100 Mbps increments be used.

Note: Sending Rate Table.

Recorded in table-steps-above-1-gbps.

RFC9097-8.1-5

Deviation

Above 10 Gbps, increments of 1 Gbps are RECOMMENDED.

Note: Sending Rate Table.

Recorded in table-steps-above-1-gbps.

RFC9097-8.1-L1

Tested

A higher initial IP-Layer Sender Bit Rate might be configured when the test operator is certain that the Maximum IP- Layer Capacity is well above the initial IP-Layer Sender Bit Rate and factors such as test duration and total test traffic play an important role.

Note: A Search Test may start at a requested srIndexConf row.

Proving tests:

RFC9097-8.1-6

Tested

The sending rate table SHOULD bracket the Maximum Capacity where it will make measurements, including constrained rates less than 500 kbps if applicable.

Note: Sending Rate Table.

Proving tests:

RFC9097-8.1-L2

Tested

Each rate is defined as datagrams of size ss, sent as a burst of count cc, each time interval tt (the default for tt is 100 microsec, a likely system tick interval).

Note: Sending Rate Structure: txInterval, udpPayload, burstSize.

Proving tests:

RFC9097-8.1-L3

Tested

While it is advantageous to use datagrams of as large a size as possible, it may be prudent to use a slightly smaller maximum that allows for secondary protocol headers and/or tunneling without resulting in IP-Layer fragmentation.

Note: Server MTU; the 1250-byte default (RFC9946-6.1-8).

Proving tests:

RFC9097-8.1-L4

Tested

At the beginning of a test, the Sender begins sending at rate R1 and the Receiver starts a feedback timer of duration FT (while awaiting inbound datagrams).

Note: Trial Interval.

Proving tests:

RFC9097-8.1-L5

Tested

As datagrams are received, they are checked for sequence number anomalies (loss, out-of-order, duplication, etc.) and the delay range is measured (one-way or round-trip).

Note: Sequence Errors and delay variation (useOwDelVar).

Proving tests:

RFC9097-8.1-L6

Tested

This information is accumulated until the feedback timer FT expires and a status feedback message is sent from the Receiver back to the Sender, to communicate this information.

Note: Status PDU.

Proving tests:

RFC9097-8.1-L7

Tested

The accumulated statistics are then reset by the Receiver for the next feedback interval.

Proving tests:

RFC9097-8.1-L8

Tested

As feedback messages are received back at the Sender, they are evaluated to determine how to adjust the current offered load rate (Rx).

Proving tests:

RFC9097-8.1-L9

Tested

If the feedback indicates that no sequence number anomalies were detected AND the delay range was below the lower threshold, the offered load rate is increased.

Note: Clean Interval.

Proving tests:

RFC9097-8.1-L10

Tested

If congestion has not been confirmed up to this point (see below for the method for declaring congestion), the offered load rate is increased by more than one rate setting (e.g., Rx+10).

Note: Fast Mode.

Proving tests:

RFC9097-8.1-L11

Tested

Conversely, if congestion has been previously confirmed, the offered load rate is only increased by one (Rx+1).

Note: Single-Step Mode.

Conformance Records: shallow-queue-search-alternates-at-the-bottleneck.

Proving tests:

RFC9097-8.1-L12

Tested

However, if a rate threshold above a high sending rate (such as 1 Gbps) is exceeded, the offered load rate is only increased by one (Rx+1) in any congestion state.

Conformance Records: fast-mode-stops-at-the-gigabit-row.

Proving tests:

RFC9097-8.1-L13

Tested

If the feedback indicates that sequence number anomalies were detected OR the delay range was above the upper threshold, the offered load rate is decreased.

Note: Errored Interval.

Proving tests:

RFC9097-8.1-7

Tested

The RECOMMENDED threshold values are 10 for sequence number gaps and 30 msec for lower and 90 msec for upper delay thresholds, respectively.

Note: seqErrThresh, lowThresh, upperThresh; RFC 9946 section 5.1 item 6.

Proving tests:

RFC9097-8.1-L14

Tested

Also, if congestion is now confirmed for the first time by the current feedback message being processed, then the offered load rate is decreased by more than one rate setting (e.g., Rx-30).

Note: Congestion Confirmed.

Proving tests:

RFC9097-8.1-L15

Tested

In all other cases, the offered load rate is only decreased by one (Rx-1).

Proving tests:

RFC9097-8.1-L16

Tested

If the feedback indicates that there were no sequence number anomalies AND the delay range was above the lower threshold but below the upper threshold, the offered load rate is not changed.

Note: Hold Interval.

Proving tests:

RFC9097-8.1-L17

Tested

Lastly, the method for inferring congestion is that there were sequence number anomalies AND/OR the delay range was above the upper threshold for three consecutive feedback intervals.

Note: Congestion; slowAdjThresh.

Proving tests:

RFC9097-8.1-8

Tested

The load rate adjustment algorithm MUST include timers that stop the test when received packet streams cease unexpectedly.

Note: ADR 0002; RFC 9946 section 5.1 item 7.

Proving tests:

RFC9097-8.1-9

Tested

The load packet timeout SHALL be reset to the configured value each time a load packet is received.

Note: ADR 0002; RFC 9946 section 5.1 item 8.

Proving tests:

RFC9097-8.1-10

Tested

If the timeout expires, the Receiver SHALL be closed and no further feedback sent.

Note: Load packet timeout. ADR 0002.

Proving tests:

RFC9097-8.1-11

Tested

The feedback message timeout SHALL be reset to the configured value each time a feedback message is received.

Note: ADR 0002; RFC 9946 section 5.1 item 9.

Proving tests:

RFC9097-8.1-12

Tested

If the timeout expires, the Sender SHALL be closed and no further load packets sent.

Note: The Status PDU timeout, RFC 9097’s “feedback message” timeout. ADR 0002.

Proving tests:

RFC9097-8.1-L18

Tested

If no status feedback messages arrive at the Sender for the interval greater than the Lost Status Backoff timeout:

Note: Lost Status Backoff: UDRT + (2+w)*FT, from the last message of any type received.

Proving tests:

RFC9097-8.1-L19

Tested

Beginning when the last message (of any type) was successfully received at the Sender:

Note: Lost Status Backoff timer start.

Proving tests:

RFC9097-8.1-13

Deviation

The offered load SHALL then be decreased, following the same process as when the feedback indicates the presence of one or more sequence number anomalies OR the delay range was above the upper threshold (as described above), with the same load rate adjustment algorithm variables in their current state.

Note: Lost Status Backoff.

Recorded in no-lost-status-backoff-upstream.

Conformance Records: lost-status-backoff-is-an-errored-interval.

Proving tests:

RFC9097-8.1-14

Tested

The RECOMMENDED initial value for w is 0, taking a Round-Trip Time (RTT) of less than FT into account.

Note: Lost Status Backoff.

Conformance Records: lost-status-backoff-initial-w.

Proving tests:

RFC9097-8.1-L20

Tested

A test with an RTT longer than FT is a valid reason to increase the initial value of w appropriately.

Note: Lost Status Backoff.

Conformance Records: lost-status-backoff-initial-w.

Proving tests:

RFC9097-8.1-15

Tested

Variable w SHALL be incremented by one whenever the Lost Status Backoff timeout is exceeded.

Note: Lost Status Backoff.

Proving tests:

RFC9097-8.1-L21

Tested

Also, if congestion is now confirmed for the first time by a Lost Status Backoff timeout, then the offered load rate is decreased by more than one rate setting (e.g., Rx-30).

Note: Lost Status Backoff.

Conformance Records: lost-status-backoff-is-an-errored-interval.

Proving tests:

RFC9097-8.1-L22

Tested

In all other cases, the offered load rate is only decreased by one (Rx-1).

Note: Lost Status Backoff.

Proving tests:

RFC9097-8.2-L1

Operational

It is of course necessary to calibrate the equipment performing the IP-Layer Capacity measurement, to ensure that the expected capacity can be measured accurately and that equipment choices (processing speed, interface bandwidth, etc.) are suitably matched to the measurement range.

Note: Deployment of the measurement hosts.

RFC9097-8.2-L2

Operational

Samples exhibiting supra-threshold packet losses or increasing queue occupation require a repeated search and/or test at a reduced fixed Sender rate for Qualification.

Note: The Operator repeats the Test; a Fixed-Rate Test serves for Qualification.

RFC9097-8.3-1

Operational

In general, it is RECOMMENDED to locate test endpoints as close to the intended measured link(s) as practical (for reasons of scale, this is not always possible; there is a limit on the number of test endpoints coming from many perspectives – for example, management and measurement traffic).

Note: Deployment of the measurement hosts.

RFC9097-8.3-2

Tested

The testing operator MUST set a value for the MaxHops Parameter, based on the expected path length.

Note: MaxHops setting; RFC 9946 section 10.

Proving tests:

RFC9097-8.3-3

Tested

Both load packets and status feedback messages MUST contain sequence numbers; this helps with measurements based on those packets.

Note: lpduSeqNo and spduSeqNo; RFC 9946 section 5.1 item 3.

Proving tests:

RFC9097-8.3-4

Deviation

Methods MUST be prepared to provide a short preamble transmission to activate on-demand communications access and to discard the preamble from subsequent test results.

Note: RFC 9946 defines no preamble; OB-UDPST’s JSON reports PreambleDuration 0.

Recorded in no-preamble-transmission.

RFC9097-8.3-5

Operational

If this condition is suspected, alternate configurations SHOULD be used.

Note: A shaper or policer whose burst size exceeds the Burst; the user changes the configuration.

RFC9097-8.3-L1

Tested

Each flow would be controlled using its own implementation of the load rate adjustment (search) algorithm.

Note: Each Test Connection of a Multi-Connection Test runs its own Load Rate Adjustment Algorithm.

Proving tests:

RFC9097-8.3-6

Deviation

The number of concurrent, independent tests of a path SHALL be limited to one.

Note: RFC 9946 section 4.2 says the same.

Recorded in tests-per-source-above-one.

Proving tests:

RFC9097-8.3-L2

Operational

As long as both IPv4 packets and IPv6 packets sent/received are standard-formed, this should be allowed (and the change in header size easily accounted for on a per-packet basis).

Note: Testing a v4-v6 transition mechanism; nothing in udpstp prevents it.

Proving tests:

RFC 9097 §9: Reporting Formats

The 15 Requirements of RFC 9097 §9, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested9
Deviation6

RFC9097-9-1

Tested

The Singleton IP-Layer Capacity results SHOULD be accompanied by the context under which they were measured. * Timestamp (especially the time when the maximum was observed in dtn). * Source and Destination (by IP or other meaningful ID). * Other inner Parameters of the test case (Section 4). * Outer Parameters, such as “test conducted in motion” or other factors belonging to the context of the measurement. * Result validity (indicating cases where the process was somehow interrupted or the attempt failed). * A field where unusual circumstances could be documented, and another one for “ignore / mask out” purposes in further processing.

Proving tests:

RFC9097-9-2

Deviation

The Maximum IP-Layer Capacity results SHOULD be reported in tabular format.

Recorded in compact-human-final-metrics.

Proving tests:

RFC9097-9-3

Deviation

There SHOULD be a column that identifies the test Phase.

Note: Maximum IP-Layer Capacity table.

Recorded in compact-human-final-metrics.

Conformance Records: results-name-the-phase-and-f.

Proving tests:

RFC9097-9-4

Deviation

There SHOULD be a column listing the number of flows used in that Phase.

Recorded in compact-human-final-metrics.

Conformance Records: results-name-the-phase-and-f.

Proving tests:

RFC9097-9-5

Deviation

The remaining columns SHOULD report the following results for the aggregate of all flows, including the Maximum IP-Layer Capacity, the Loss Ratio, the RTT minimum, RTT maximum, and other metrics tested having similar relevance.

Recorded in compact-human-final-metrics.

Proving tests:

RFC9097-9-6

Tested

As mentioned in Section 6.6, bimodal (or multi-modal) maxima SHALL be reported for each mode separately.

Note: Capacity Modes; restates RFC9097-6.6-3.

Proving tests:

RFC9097-9-7

Tested

The sub-interval time, dt, MUST accompany a report of Maximum IP- Layer Capacity results, as well as the remaining Parameters from Section 4 (“General Parameters and Definitions”).

Proving tests:

RFC9097-9-8

Tested

The PM list metrics corresponding to the sub-interval where the Maximum Capacity occurred MUST accompany a report of Maximum IP-Layer Capacity results, for each test Phase.

Proving tests:

RFC9097-9-9

Tested

The IP-Layer Sender Bit Rate results SHOULD be reported in tabular format.

Proving tests:

RFC9097-9-10

Tested

There SHOULD be a column that identifies the test Phase.

Note: The table of IP-Layer Sender Bit Rates.

Conformance Records: results-name-the-phase-and-f.

Proving tests:

RFC9097-9-11

Deviation

There SHOULD be a column listing each individual (numbered) flow used in that Phase, or the aggregate of flows in that Phase.

Recorded in sender-bit-rate-per-sub-interval.

RFC9097-9-12

Deviation

A corresponding column SHOULD identify the specific sending rate sub- interval, stn, for each flow and aggregate.

Recorded in sender-bit-rate-per-sub-interval.

RFC9097-9-13

Tested

A final column SHOULD report the IP-Layer Sender Bit Rate results for each flow used, or the aggregate of all flows.

Proving tests:

RFC9097-9-14

Tested

The sub-interval duration, st, MUST accompany a report of Sender IP- Layer Bit Rate results.

Proving tests:

RFC9097-9-15

Tested

Also, the values of the remaining Parameters from Section 4 (“General Parameters and Definitions”) MUST be reported.

Proving tests:

RFC 9097 §10: Security Considerations

The 7 Requirements of RFC 9097 §10, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested6
Operational1

RFC9097-10-1

Operational

Cooperating Source and Destination hosts and agreements to test the path between the hosts are REQUIRED.

Note: Agreements between the parties that run the hosts.

RFC9097-10-2

Tested

It is REQUIRED to have a user client-initiated setup handshake between cooperating hosts that allows firewalls to control inbound unsolicited UDP traffic that goes to either a control port (expected and with authentication) or ephemeral ports that are only created as needed.

Note: Setup exchange on the Standard Port; Test Connections on Ephemeral Ports.

Proving tests:

RFC9097-10-3

Tested

Client-server authentication and integrity protection for feedback messages conveying measurements are RECOMMENDED.

Note: Authentication Mode 2 (RFC 9946 section 5.3.2).

Proving tests:

RFC9097-10-4

Tested

Hosts MUST limit the number of simultaneous tests to avoid resource exhaustion and inaccurate results.

Note: Admission.

Proving tests:

RFC9097-10-5

Tested

Senders MUST be rate limited.

Note: Sending Rate Table; Admitted Rate Cap.

Proving tests:

RFC9097-10-L1

Tested

The recommended load control search algorithm results in “ramp-up” from the lowest rate in the table.

Note: A Search Test starts at row 0 unless srIndexConf asks otherwise.

Proving tests:

RFC9097-10-6

Tested

Testing with the Service Provider’s measurement hosts SHOULD be limited in frequency and/or overall volume of test traffic (for example, the range of duration values, I, SHOULD be limited).

Note: testIntTime’s maximum.

Proving tests:

RFC 9946 §2: Conventions and Terminology

The 1 Requirements of RFC 9946 §2, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Not applicable1

RFC9946-2-1

Not applicable

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Note: BCP 14 boilerplate: it defines the keywords and requires nothing of an endpoint.

RFC 9946 §3: Scope, Goals, and Applicability

The 1 Requirements of RFC 9946 §3, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Operational1

RFC9946-3-L1

Operational

Per [RFC9097], that algorithm must not be used as a general Congestion Control Algorithm (CCA).

Note: A usage statement; RFC9946-5.1-3 says it with a keyword.

RFC 9946 §4: Protocol Overview

The 15 Requirements of RFC 9946 §4, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested8
Deviation1
Operational6

RFC9946-4-1

Tested

All messages defined by this document SHALL use UDP transport [RFC0768].

Proving tests:

RFC9946-4-L1

Tested

It is the responsibility of the client process to manage the inter-related connections such as handling the individual connection setup successes and failures, cleaning up connections during a test (should some fail), and aggregating the individual test results into an overall set of performance statistics.

Note: Multi-Connection Test: Minimum Test Connections, failed Test Connections, the aggregate Sub-Interval.

Conformance Records: minimum-test-connections, multi-connection-isolated-failures, aggregate-clock-delta-min.

Proving tests:

RFC9946-4-L2

Tested

The server instance(s) would process each connection independently, as if they were coming from separate clients.

Conformance Records: multi-connection-agreement-checks.

Proving tests:

RFC9946-4-L3

Tested

Test Setup Request and Response: If a server instance is identified with a host name that resolves to both IPv4/IPv6 addresses, it is recommended to use the first address returned in the name resolution response, regardless of whether it is IPv4 or IPv6.

Note: Client host name resolution.

Proving tests:

RFC9946-4-L4

Tested

Distinct UDP port numbers will be assigned if all Setup Response messages originate from the same server in that case.

Note: An Ephemeral Port per Test Connection.

Proving tests:

RFC9946-4.1-1

Operational

Fixed-rate testing SHOULD only be activated for operation and maintenance purposes by operators within their local network domain.

Note: Who runs a Fixed-Rate Test, and where; the software limits it to Operator Keys (RFC9946-4.1-3).

RFC9946-4.1-2

Operational

If a subscriber requests a diagnostic test from the network operator, it strongly implies that there is no certainty on the bottleneck capacity and initiating a UDP Speed Test based on the load adjustment algorithm is RECOMMENDED.

Note: An operator’s choice of test.

RFC9946-4.1-3

Tested

To protect against misuse, a client (and in general, a consumer) MUST NOT be able to initiate a fixed-rate test.

Note: A Fixed-Rate Test needs an Operator Key.

Conformance Records: fixed-rate-needs-operator-key.

Proving tests:

RFC9946-4.1-L1

Tested

A network operator may conduct a fixed-rate test for a stable measurement at or near the maximum determined by the load rate adjustment algorithm for debugging purposes.

Note: srIndexConf a row with CHTA_SRIDX_ISSTART clear, under an Operator Key.

Proving tests:

RFC9946-4.2-L1

Operational

Measurements that generate traffic on shared paths (including Wi-Fi and Internet paths) need to consider the impact on other traffic.

Note: A deployment statement.

RFC9946-4.2-L2

Operational

Fixed-rate testing operates without congestion control and therefore must not be executed over other operators’ network segments.

Note: Where an operator runs a Fixed-Rate Test.

RFC9946-4.2-L3

Operational

Fixed- rate testing, therefore, is limited to paths within a domain entirely managed and operated section-wise and end-to-end by the network operator performing the measurement.

Note: Where an operator runs a Fixed-Rate Test.

RFC9946-4.2-L4

Operational

When the risks of disruption to other flows has been considered, testing could be extended to include adjacent operational domains for which there is also a testing agreement.

Note: Where an operator runs a Fixed-Rate Test.

RFC9946-4.2-1

Deviation

The number of concurrent independent tests of a path SHALL be limited to one, regardless of the number of flows.

Note: Server admission: one activated Test per source.

Recorded in tests-per-source-above-one.

Conformance Records: path-approximated-by-source.

Proving tests:

RFC9946-4.2-L5

Tested

A load rate adjustment algorithm (see Section 5.1) is required to mitigate the impact of this congestion and to limit the duration of any congestion by terminating the test when sudden impairments or a loss of connectivity is detected.

Conformance Records: multi-connection-path-failure.

Proving tests:

RFC 9946 §5: Requirements, Security Operations, and Optional Checksum

The 73 Requirements of RFC 9946 §5, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested57
Deviation3
Operational8
Not applicable5

RFC9946-5-L1

Tested

Due to the additional complexities, and loss of the direct mapping of packets to datagrams between Layer 3 and Layer 4, it is recommended that Layer 3 fragmentation be avoided.

Note: Default datagram sizes fit a traditional MTU.

Proving tests:

RFC9946-5-1

Deviation

Unless IP fragmentation is expected, and is one of the attributes being measured, the IPv4 Don’t Fragment (DF) bit SHOULD be set for all tests.

Note: The dont_fragment setting, on by default.

Recorded in windows-dont-fragment-unset.

Proving tests:

RFC9946-5-L2

Tested

Note: When this specification is used for network debugging, it may be useful for fragmentation to be under the control of the test administrator.

Note: The dont_fragment setting.

Proving tests:

RFC9946-5-2

Not applicable

This section specifies generic requirements, which a measurement load rate adjustment algorithm conforming to this specification MUST fulfill.

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9946-5.1-1

Not applicable

Load rate adjustment algorithms for capacity measurement MUST comply with the requirements specified by this section.

Note: An introduction to the section’s Requirements, each recorded with its own ID; nothing to do beyond them.

RFC9946-5.1-2

Not applicable

New standard load rate adjustment algorithms for capacity measurement MUST be reviewed by IETF designated experts prior to assignment of a code point in the “Test Activation PDU Rate Adjustment Algo” registry.

Note: An IANA registration process, not endpoint behaviour.

RFC9946-5.1-3

Operational

The measurement load rate adjustment algorithm described in this section MUST NOT be used as a general CCA.

Note: A usage statement: udpstp runs the algorithm only within a Test.

RFC9946-5.1-4

Operational

This specification MUST only be used in the application of diagnostic and operations measurements.

Note: A usage statement.

RFC9946-5.1-5

Tested

Both Load PDU messages and Status Feedback PDU messages MUST contain sequence numbers.

Note: lpduSeqNo and spduSeqNo fields.

Proving tests:

RFC9946-5.1-6

Tested

The nominal duration of a measurement interval at the Destination, parameter testIntTime (“I” in [RFC9097]), MUST default to a value of no more than 10 seconds.

Note: testIntTime default.

Proving tests:

RFC9946-5.1-7

Tested

A high-speed mode to achieve high sending rates quickly MUST reduce the measurement load below a level for which the first feedback interval inferred “congestion” from the measurements.

Note: Fast Mode’s drop when Congestion is confirmed.

Conformance Records: fast-mode-stops-at-the-gigabit-row, confirming-drop-below-the-gigabit-row-only.

Proving tests:

RFC9946-5.1-L1

Tested

Consecutive feedback intervals that have a supra-threshold count of sequence number anomalies and/or contain an upper delay variation threshold exception in all of the consecutive intervals indicate “congestion” within a test.

Note: Congestion Confirmed after slowAdjThresh Errored Intervals back to back (ADR 0005).

Conformance Records: latched-congestion-confirmed.

Proving tests:

RFC9946-5.1-8

Deviation

The threshold of consecutive feedback intervals SHALL be configurable with a default of 3 intervals and a maximum duration to infer congestion of 500 ms (milliseconds).

Note: slowAdjThresh and trialInt.

Recorded in operator-keys-exempt-from-500-ms-congestion-inference.

Conformance Records: congestion-inference-within-500-ms.

Proving tests:

RFC9946-5.1-9

Tested

Congestion MUST be indicated if the Status Feedback PDUs indicate that either sequence number anomalies were detected OR the delay range was above the upper delay variation threshold.

Note: seqErrThresh and upperThresh.

Conformance Records: clean-needs-delay-var-below-low-thresh, seq-err-thresh-always-applied, no-delay-sample-is-never-clean.

Proving tests:

RFC9946-5.1-10

Tested

The RECOMMENDED threshold values are 10 for sequence number gaps, 30 ms for lower delay variation thresholds, and 90 ms for upper delay variation thresholds.

Note: seqErrThresh, lowThresh and upperThresh defaults.

Proving tests:

RFC9946-5.1-11

Tested

The load rate adjustment algorithm MUST include a Load PDU timeout and a Status PDU timeout, which both stop the test when received PDU streams cease unexpectedly.

Note: ADR 0002 reconciles them with the Section 6.1 Watchdog.

Conformance Records: receive-timeouts-stop-sending-at-1s, receive-timeouts-run-from-data-phase-start.

Proving tests:

RFC9946-5.1-12

Tested

The Load PDU timeout SHALL be reset to the configured value each time a Load PDU is received.

Proving tests:

RFC9946-5.1-13

Tested

If the Load PDU timeout expires, the receiver SHALL be closed and no further Status PDU feedback sent.

Conformance Records: receive-timeout-is-final.

Proving tests:

RFC9946-5.1-14

Tested

The default Load PDU timeout MUST be no more than 1 second.

Proving tests:

RFC9946-5.1-15

Tested

The Status PDU timeout SHALL be reset to the configured value each time a feedback message is received.

Proving tests:

RFC9946-5.1-16

Tested

If the Status PDU timeout expires, the sender SHALL be closed and no further load packets sent.

Conformance Records: receive-timeout-is-final.

Proving tests:

RFC9946-5.1-17

Tested

The default Status PDU timeout MUST be no more than 1 second.

Proving tests:

RFC9946-5.1-18

Tested

A network operator who is certain of the IP-Layer Capacity to be validated MAY start with a fixed-rate test at the IP-Layer Capacity and avoid activating the measurement load rate adjustment algorithm (see Section 8.1 of [RFC9097]).

Note: A Fixed-Rate Test under an Operator Key.

Proving tests:

RFC9946-5.1-19

Operational

However, the stimulus for a diagnostic test (such as a subscriber request) strongly implies that there is no certainty, and the load adjustment algorithm is RECOMMENDED.

Note: An operator’s choice of test.

RFC9946-5.1-20

Operational

This specification MUST only be used in circumstances consistent with Section 10 (Security Considerations) of [RFC9097].

Note: A usage statement.

RFC9946-5.3-1

Deviation

A REQUIRED mode with authentication during the Control phase (Test Setup and Test Activation exchanges).

Note: Authentication Mode 1; authMode field.

Recorded in unauthenticated-mode.

Proving tests:

RFC9946-5.3-2

Tested

An OPTIONAL mode with the additional authentication of the Status Feedback messages during the Data phase.

Note: Authentication Mode 2; authMode field.

Proving tests:

RFC9946-5.3.1-1

Tested

In this mode, the client and the server SHALL be configured to use one of a number of shared secret keys, designated via the numeric keyId field (see Section 5.4).

Note: keyId field; the Key Table.

Proving tests:

RFC9946-5.3.1-2

Tested

This key SHALL be used as input to the KDF, as specified in Section 5.4.1, to obtain the actual keys used by the client and server for authentication.

Proving tests:

RFC9946-5.3.1-3

Tested

During the Control phase, the sender SHALL read the current system (wall-clock) time and populate the authUnixTime field and next calculate the 32-octet HMAC-SHA-256 hash of the entire PDU according to Section 6 of [RFC6234] (with the authDigest and checkSum preset to all zeroes).

Note: authUnixTime and authDigest fields.

Proving tests:

RFC9946-5.3.1-L1

Tested

The authDigest field is filled by the result, then the packet is sent to the receiver.

Note: authDigest field.

Proving tests:

RFC9946-5.3.1-4

Tested

The value in the authUnixTime field is a 32-bit timestamp, and a 10-second tolerance window (+/- 5 seconds) SHALL be used by the receiver to distinguish a subsequent replay of a PDU.

Note: authUnixTime field; the replay cache.

Conformance Records: auth-time-window-inclusive-modulo-2-32, replay-cache-for-setup-requests, replay-cache-activation-keyed-on-sender, replay-cache-lifetime-and-capacity.

Proving tests:

RFC9946-5.3.1-L2

Not applicable

See Table 2 of [TR-471] for a recommended timestamp resolution.

Note: A pointer to TR-471’s recommended timestamp resolution, not a behaviour; authUnixTime is whole seconds as the PDU defines it.

RFC9946-5.3.1-5

Tested

Upon reception, the receiver SHALL validate the message PDU for correct length, validity of authDigest, immediacy of authUnixTime, and expected formatting (PDU-specific fields are also checked, such as protocol version).

Proving tests:

RFC9946-5.3.1-L3

Tested

Validation of the authDigest requires that it be extracted from the PDU and the field, along with the checkSum field, zeroed prior to the Hashed Message Authentication Code (HMAC) calculation used for comparison (see Section 7.2 of [RFC9145]).

Proving tests:

RFC9946-5.3.1-6

Tested

If the validation fails, the receiver SHOULD NOT continue with the Control phase and SHOULD implement silent rejection (no further packets sent on the address/port pairs).

Proving tests:

RFC9946-5.3.1-L4

Tested

The exception is when the testing hosts have been configured for troubleshooting Control phase failures and rejection messages will aid in the process.

Note: Permits reporting a failed validation when configured for troubleshooting.

Proving tests:

RFC9946-5.3.1-7

Tested

If the validation succeeds, the receiver SHALL continue with the Control phase and compose a successful response or a response indicating the error conditions identified (if any).

Proving tests:

RFC9946-5.3.1-8

Tested

This process SHALL be executed for the request and response in the Test Setup exchange, including the Null Request (Section 6) and the Test Activation exchange (Section 7).

Proving tests:

RFC9946-5.3.2-1

Tested

When using the optional authentication during the Data phase, authentication SHALL also be applied to the Status Feedback PDU (see Section 8.2).

Proving tests:

RFC9946-5.3.2-2

Tested

The Status PDU sender SHALL 1) read the current system (wall-clock) time and populate the authUnixTime field, 2) calculate the authDigest field of the entire Status PDU (with the authDigest and checkSum preset to all zeroes), and 3) send the packet to the receiver.

Note: Status PDU authUnixTime and authDigest fields.

Proving tests:

RFC9946-5.3.2-L1

Tested

The values of authUnixTime field and authDigest field are determined as defined by Section 5.3.1.

Proving tests:

RFC9946-5.3.2-3

Tested

Upon reception, the receiver SHALL validate the message PDU for correct length, validity of authDigest, immediacy of authUnixTime, and expected formatting (PDU-specific fields are also checked, such as protocol version).

Proving tests:

RFC9946-5.3.2-L2

Tested

Validation of the authDigest will require that it be extracted from the PDU and the field, along with the checkSum field, zeroed prior to the HMAC calculation used for comparison.

Proving tests:

RFC9946-5.3.2-4

Tested

If the authentication validation fails, the receiver SHALL ignore the message.

Proving tests:

RFC9946-5.3.2-5

Tested

If the watchdog timer expires (due to successive failed validations), the test session will prematurely terminate (and no further load traffic SHALL be transmitted).

Proving tests:

RFC9946-5.3.2-6

Tested

If this optional mode has not been selected, then the keyId, authUnixTime, and authDigest fields of the Status PDU (see Section 8.2) SHALL be set to all zeroes.

Note: Status PDU keyId, authUnixTime and authDigest fields.

Conformance Records: status-pdu-auth-fields-ignored-outside-mode-2.

Proving tests:

RFC9946-5.4-1

Tested

The key table SHALL be used with the REQUIRED authentication mode and the OPTIONAL authentication mode (using the same key).

Proving tests:

RFC9946-5.4-2

Tested

For authentication, this key SHALL only be used as input to the KDF, as specified in Section 5.4.1, to derive the actual keys used for authentication processing.

Proving tests:

RFC9946-5.4-3

Tested

The key table SHALL have (at least) the following fields per Section 2 of [RFC7210]: * AdminKeyName * LocalKeyName * KDF * AlgID * Key * SendLifetimeStart * SendLifeTimeEnd * AcceptLifeTimeStart * AcceptLifeTimeEnd

Note: The TOML Key Table.

Conformance Records: key-table-lifetimes, key-table-alg-id, key-table-operator-extension.

Proving tests:

RFC9946-5.4-4

Tested

The LocalKeyName SHALL be determined from the corresponding keyId field in the PDUs that follow.

Note: keyId field.

Conformance Records: key-table-alg-id.

Proving tests:

RFC9946-5.4.1-1

Tested

The KDF algorithm SHALL be a Key Derivation Function in Counter Mode, as specified in Section 4.1 of [NIST800-108].

Proving tests:

RFC9946-5.4.1-2

Tested

The Pseudorandom Function (PRF) used in the KDF SHALL be HMAC-SHA-256, as defined in Section 6 of [RFC6234].

Proving tests:

RFC9946-5.4.1-3

Tested

The KDF SHALL use the following parameters: * Kin (key-derivation key): The shared key as identified by the keyId field in the PDU. * Label: The fixed string “UDPSTP” (without quotes), encoded as a UTF-8 string, used to bind the derived keys to this specific protocol. * Context: The UTF-8 string representation of the authUnixTime field received in the very first Setup Request PDU sent from the client to the server. This ensures that the derived keys are unique to the session and tied to the temporal context of the initial setup exchange. The authUnixTime field serves as a nonce and is protected from modification by the HMAC-SHA-256 hash present in the authDigest field. * r: The length of the binary encoding of the counter SHALL be 32 (bits).

Note: The fixed input is laid out as Appendix A builds it.

Conformance Records: kdf-appendix-a-layout.

Proving tests:

RFC9946-5.4.1-4

Tested

The length of the binary encoding of the counter SHALL be 32 (bits).

Note: The r parameter.

Proving tests:

RFC9946-5.4.1-5

Tested

The total derived key material SHALL be 512 bits (64 octets) in length.

Proving tests:

RFC9946-5.4.1-6

Tested

The key material SHALL be structured as follows, from most significant bit (MSB) to least significant bit (LSB): * Client Authentication Key: 256 bits (32 octets); used for authenticating messages sent by the client. * Server Authentication Key: 256 bits (32 octets); used for authenticating messages sent by the server.

Proving tests:

RFC9946-5.4.1-7

Tested

If authentication of the initial Setup Request PDU received by the server fails, due to an invalid authDigest field, any and all derived keying material and keys SHALL be considered invalid.

Conformance Records: derived-keys-depend-only-on-the-row-and-time.

Proving tests:

RFC9946-5.4.1-8

Tested

The key material derived from the initial Setup Request PDU, either at the client prior to transmission or at the server upon reception, SHALL be used for all subsequent PDUs sent between them for that test connection.

Conformance Records: activation-auth-matches-setup.

Proving tests:

RFC9946-5.5-L1

Operational

Successful interaction with a local firewall assumes the firewall is configured to allow a host to open a bidirectional connection using unique source and destination addresses as well as port numbers (i.e., a 4-tuple) by sending a packet using that 4-tuple for a given transport protocol.

Note: Client-side firewall configuration.

RFC9946-5.5-1

Operational

The firewall at the server MUST be configured with an open pinhole for the server IP address and standard UDP port number of the server.

Note: Server firewall configuration.

RFC9946-5.5-2

Tested

Assuming that the firewall administration at the server does not allow an open UDP ephemeral port range, then the server MUST send a Null Request to the client from the ephemeral port number communicated to the client in the Test Setup Response.

Note: Null Request, sent from the Ephemeral Port.

Proving tests:

RFC9946-5.5-L2

Tested

If the server firewall administration allows an open UDP ephemeral port range, then the Null Request is not strictly necessary.

Note: Permits leaving out the Null Request.

Proving tests:

RFC9946-5.6-1

Operational

The protocol MUST utilize the standard UDP checksum for all IPv4 and IPv6 datagrams it sends.

Note: The operating system’s UDP stack computes the UDP checksum; udpstp never turns it off (no SO_NO_CHECK), and IPv6 requires it.

RFC9946-5.6-L1

Tested

All of the PDUs exchanged between the client and server support an optional header checksum that covers the various fields in the UDPSTP PDU (excluding the payload content of the Load PDU and, to be clear, also the IP and UDP headers).

Note: checkSum field.

Proving tests:

RFC9946-5.6-L2

Tested

The calculation is the same as the 16-bit one’s complement Internet checksum used in the IPv4 packet Header Checksum specification (see Section 3.1 of [RFC0791]).

Note: checkSum field.

Proving tests:

RFC9946-5.6-2

Operational

However, all UDPSTP datagrams transmitted via IPv4 or IPv6 SHALL include a standard UDP checksum to protect other potential recipients to whom a corrupted packet may be delivered.

Note: The operating system’s UDP stack computes the UDP checksum; udpstp never turns it off (no SO_NO_CHECK), and IPv6 requires it.

RFC9946-5.6-L3

Not applicable

In the case of a nonstandard network API, one option to reduce processing overhead may be to restrict testing to only utilize a payload content of all zeros so that the UDP checksum calculation need not include it for Load PDUs.

Note: udpstp sends through the standard socket API.

RFC9946-5.6-3

Tested

If a PDU sender is populating the checkSum field, it SHALL do so as the last step after the PDU is built in all other respects (with the checkSum field set to zero prior to the calculation).

Note: checkSum field.

Conformance Records: zero-checksum-sent-as-ffff.

Proving tests:

RFC9946-5.6-4

Tested

The PDU receiver SHALL subsequently verify the PDU checksum whenever checksum processing has been configured and the field is populated.

Note: checkSum field.

Proving tests:

RFC9946-5.6-5

Tested

If PDU checksum validation fails, the PDU SHALL be discarded.

Proving tests:

RFC9946-5.6-6

Tested

Because of the redundancy when used in conjunction with authentication, it is OPTIONAL for a PDU sender to utilize the UDPSTP checkSum field.

Note: checkSum field.

Proving tests:

RFC9946-5.6-7

Tested

However, because authentication is not applicable to the Load PDU, the checkSum field SHALL be utilized by the sender whenever UDP data integrity may be uncertain (as outlined above).

Note: Load PDU checkSum field.

Proving tests:

RFC 9946 §6: Test Setup Request and Response

The 60 Requirements of RFC 9946 §6, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested58
Deviation2

RFC9946-6-1

Tested

The client source IP address and the server destination IP address MUST NOT be a broadcast or multicast address.

Conformance Records: broadcast-means-limited-broadcast.

Proving tests:

RFC9946-6-2

Tested

Any Test Setup Request or Test Setup Response packet containing a multicast or broadcast source or destination IP address MUST be silently dropped and ignored.

Conformance Records: broadcast-means-limited-broadcast.

Proving tests:

RFC9946-6-L1

Tested

The measurement method and the protocol specified by this document are expected to function with unicast and anycast IP addresses.

Note: Replies leave from the arrival address (ADR 0007).

Proving tests:

RFC9946-6.1-1

Tested

The client SHALL begin the Control phase exchange by sending a Test Setup Request message to the server’s (standard) control port.

Proving tests:

RFC9946-6.1-2

Tested

The client SHALL simultaneously start a test initiation timer so that if the Control phase fails to complete Test Setup and Test Activation exchanges in the allocated time, the client software SHALL exit (i.e., the UDP socket will be closed and an error message will be displayed to the user).

Conformance Records: multi-connection-setup-deadline.

Proving tests:

RFC9946-6.1-3

Tested

The test initiation timer MAY reuse the test termination timeout value.

Proving tests:

RFC9946-6.1-4

Tested

Note: Any field labeled as ‘reserved for alignment’, in any PDU, MUST be set to 0 and MUST be ignored upon receipt.

Proving tests:

RFC9946-6.1-5

Tested

The UDP PDU format layout SHALL be as follows (big-endian AB, starting with the most significant byte and ending with the least significant byte):

Proving tests:

RFC9946-6.1-6

Tested

This field MAY be used to rate-limit the maximum rate the server should attempt.

Conformance Records: max-bandwidth-split-across-test-connections, search-capped-at-stated-max-bandwidth.

Proving tests:

RFC9946-6.1-7

Tested

This bit SHALL be set by default.

Proving tests:

RFC9946-6.1-8

Tested

By default, sending rates up to 1 Gbps SHALL NOT produce IP packet sizes greater than 1250 bytes (unless CHSR_TRADITIONAL_MTU is set), while rates above 1 Gbps MAY produce IP packet sizes up to 9000 bytes.

Conformance Records: ipv4-based-sending-rate-structure-sizes, sending-rate-table-follows-the-server-shape.

Proving tests:

RFC9946-6.1-9

Tested

When CHSR_JUMBO_STATUS is not set, all sending rates SHALL NOT produce IP packet sizes greater than 1250 bytes (unless CHSR_TRADITIONAL_MTU is set).

Conformance Records: ipv4-based-sending-rate-structure-sizes, sending-rate-table-follows-the-server-shape.

Proving tests:

RFC9946-6.1-10

Tested

This bit SHALL NOT be set by default.

Proving tests:

RFC9946-6.1-11

Tested

If set, sending rates up to 1 Gbps MAY produce IP packets up to the traditional size of 1500 bytes.

Conformance Records: ipv4-based-sending-rate-structure-sizes, sending-rate-table-follows-the-server-shape.

Proving tests:

RFC9946-6.1-12

Tested

If CHSR_JUMBO_STATUS is simultaneously not set, all sending rates SHALL NOT produce IP packets greater than the traditional size of 1500 bytes.

Conformance Records: ipv4-based-sending-rate-structure-sizes, sending-rate-table-follows-the-server-shape.

Proving tests:

RFC9946-6.1-13

Tested

This field MUST be set to 0 and MUST be ignored upon receipt.

Proving tests:

RFC9946-6.1-L1

Tested

A one-octet field indicating the index of a connection relative to all connections that make up a single test (starting at 0, incremented by 1 per connection).

Note: mcIndex field.

Conformance Records: multi-connection-agreement-checks.

Proving tests:

RFC9946-6.1-L2

Tested

An implementation may restrict the number of connections supported for a single test to a value less than or equal to 255.

Note: mcIndex field.

Proving tests:

RFC9946-6.1-L3

Tested

A two-octet field containing a pseudorandom non-zero identifier (via a Random Number Generator, source port number, …) that is common to all connections of a single test.

Note: mcIdent field.

Conformance Records: mc-ident-from-a-held-port, multi-connection-agreement-checks.

Proving tests:

RFC9946-6.1-L4

Tested

A one-octet field set to CHSR_CREQ_SETUPREQ to indicate a Setup Request message.

Note: cmdRequest field.

Proving tests:

RFC9946-6.1-L5

Tested

Note that CHSR_CREQ_NONE remains unused.

Note: cmdRequest field.

Proving tests:

RFC9946-6.1-L6

Tested

All Request PDUs always have a Command Response of XXXX_CRSP_NONE (i.e., CHSR_CRSP_NONE, CHNR_CRSP_NONE, or CHTA_CRSP_NONE).

Note: cmdResponse field.

Proving tests:

RFC9946-6.1-L7

Tested

The server compares this value to its currently available configured limit for test admission control.

Note: maxBandwidth field.

Conformance Records: max-bandwidth-split-across-test-connections, admitted-rate-cap-per-direction.

Proving tests:

RFC9946-6.1-L8

Tested

The maxBandwidth field’s most significant bit, the CHSR_USDIR_BIT, is set to 0 by default to indicate “downstream” and has to be set to 1 to indicate “upstream”.

Note: maxBandwidth field.

Conformance Records: usdir-set-on-every-upstream-setup, admitted-rate-cap-per-direction, usdir-must-match-activation-direction.

Proving tests:

RFC9946-6.1-L9

Tested

A two-octet field set to zero in the Test Setup Request and populated by the server in the Test Setup Response.

Note: testPort field.

Proving tests:

RFC9946-6.1-L10

Tested

It contains the UDP ephemeral port number on the server that the client has to use for the Test Activation Request and subsequent Load or Status PDUs.

Note: testPort field.

Proving tests:

RFC9946-6.1-L11

Deviation

One of the following has to be set (see Section 5.3 for requirements and details of operation): AUTHMODE_1: Required authentication for the Control phase. AUTHMODE_2: Required authentication for the Control and Data phases (Status Feedback PDU only).

Note: authMode field. Modes 1 and 2 are met; Unauthenticated Mode (0) is the Deviation.

Recorded in unauthenticated-mode.

RFC9946-6.1-L12

Tested

A 32-bit timestamp of the current system (wall-clock) time since the Unix Epoch on January 1, 1970 at 00:00:00 UTC.

Note: authUnixTime field; the low 32 bits of the wall clock (ADR 0009).

Proving tests:

RFC9946-6.1-L13

Tested

Normally, the calculation is done as the last step of building the PDU.

Note: authDigest field.

Proving tests:

RFC9946-6.1-L14

Tested

However, if the optional checkSum field is being utilized, it becomes the penultimate step and is done just prior to the checksum calculation (with the checkSum field set to zero).

Note: authDigest field.

Proving tests:

RFC9946-6.1-L15

Tested

The calculation is done as the very last step of building the PDU, with the checkSum field set to zero.

Note: checkSum field.

Proving tests:

RFC9946-6.2-1

Tested

When the server receives the Setup Request, it SHALL first perform the following: Message Verification Procedure: 1. Verify that the size of the message is correct. 2. If the optional checkSum field is being utilized, validate the checksum as described in Section 5.6 and (if valid) zero the checkSum field prior to authentication verification. 3. Verify that the authMode value is valid and appropriate (per Section 5.3) for the message type. 4. If the authMode is valid and appropriate, authenticate the message by checking the authDigest as prescribed in Section 5.3. 5. If the message is authentic, check the authUnixTime field for acceptable immediacy.

Note: The Message Verification Procedure that later sections cite as “Section 6.2, Paragraph 2”. The authUnixTime window is +/-5 s inclusive, compared modulo 2^32 (ADR 0009).

Proving tests:

RFC9946-6.2-2

Tested

Note: If any of the above checks fail, the message SHALL be considered invalid.

Conformance Records: authentic-setup-outside-time-window-is-silent.

Proving tests:

RFC9946-6.2.1-1

Tested

The server SHALL then evaluate the other fields in the protocol header, such as the protocol version, the PDU ID (to validate the type of message), the maximum bandwidth requested for the test, and the modifierBitmap for use of options such as Jumbo datagram status and Traditional MTU (1500 bytes).

Proving tests:

RFC9946-6.2.1-2

Tested

If the client has selected options for * Jumbo datagram support (modifierBitmap), * Traditional MTU (modifierBitmap), and * Authentication mode (authMode) that do not match the server configuration, the server MUST reject the Setup Request.

Conformance Records: sending-rate-table-follows-the-server-shape.

Proving tests:

RFC9946-6.2.1-3

Deviation

If the authDigest is valid, a Test Setup Response SHALL be sent back to the client with a corresponding Command Response value indicating the reason for the rejection.

Note: Applies when a Setup Request must be rejected. Met except for the refusals the Deviation leaves unanswered; the rejection codes are recorded in docs/conformance/records/control.toml. OB-UDPST also answers authentication failures.

Recorded in authentic-setup-requests-left-unanswered.

Conformance Records: authentic-setup-outside-time-window-is-silent, server-limits-refuse-with-connfail.

Proving tests:

RFC9946-6.2.1-4

Tested

If the authDigest is invalid, then the Test Setup Request SHOULD fail silently.

Note: The RFC permits server administrators to send a Setup Response anyway, for operations and troubleshooting (Troubleshooting Mode).

Proving tests:

RFC9946-6.2.1-5

Tested

The additional circumstances when a server SHALL NOT communicate the appropriate Command Response code for an error condition (fail silently) are when: * the Setup Request PDU size is not equal to the ‘struct controlHdrSR’ size shown in Figure 3, * the PDU ID is not 0xACE1 (Test Setup PDU), or * a directed attack has been detected.

Note: Attack detection is out of scope per the RFC, so the third case is not testable.

Proving tests:

RFC9946-6.2.1-6

Tested

When the server replies to a Test Setup Request message, the Test Setup Response PDU is structured identically to the Test Setup Request PDU and SHALL retain the original values received in it, with the following exceptions: * The cmdRequest field is set to CHSR_CREQ_SETUPRSP, indicating a response. * The cmdResponse field is set to an error code (starting at cmdResponse 2, Bad Protocol Version; see Section 12.3.5), indicating the reason for rejection. If cmdResponse indicates a bad protocol version (CHSR_CRSP_BADVER), the protocolVer field is also updated to indicate the current expected version. * The authUnixTime field is updated to the current system (wall- clock) time and, after the authDigest and checkSum fields are zeroed, the authDigest is recalculated and inserted. If the optional checkSum field is being utilized, it is then also calculated and inserted.

Note: The rejection case: the exceptions cover cmdRequest, cmdResponse (and protocolVer for CHSR_CRSP_BADVER), authUnixTime, authDigest and checkSum. Reserved fields are still zeroed (RFC9946-6.1-4); the Server echoes keyId (ADR 0009).

Proving tests:

RFC9946-6.2.1-7

Tested

The Setup Request/Response message PDU SHALL be organized as follows (here and in all following code figures coded by programming language C [C-Prog]):

Note: Figure 3’s struct controlHdrSR follows.

Proving tests:

RFC9946-6.2.1-L1

Tested

The exception is for operations support: server administrators are permitted to send a Setup Response to support operations and troubleshooting.

Note: Troubleshooting Mode.

Conformance Records: troubleshooting-replies-unsigned-except-authtime, troubleshooting-replies-rate-limited.

Proving tests:

RFC9946-6.2.2-1

Tested

If the server finds that the Setup Request matches its configuration and is otherwise acceptable, the server SHALL initiate a new connection to receive the Test Activation Request from the client, using a new UDP socket allocated from the UDP ephemeral port range.

Proving tests:

RFC9946-6.2.2-2

Tested

Then, the server SHALL start a watchdog timer (to terminate the new connection if the client goes silent) and SHALL send the Test Setup Response back to the client.

Note: The watchdog uses the same value as on the Client side, whose timeouts are in Section 6.1; timeouts per ADR 0002.

Proving tests:

RFC9946-6.2.2-3

Tested

When the server replies to the Test Setup Request message, the Test Setup Response PDU is structured identically to the Test Setup Request PDU and SHALL retain the original values received in it, with the following exceptions: * The cmdRequest field is set to CHSR_CREQ_SETUPRSP, indicating a response. * The cmdResponse field is set to CHSR_CRSP_ACKOK, indicating an acknowledgment. * The testPort field is set to the ephemeral port number to be used for the client’s Test Activation Request and all subsequent communication. * The authUnixTime field is updated to the current system (wall- clock) time and, after the authDigest and checkSum fields are zeroed, the authDigest is recalculated and inserted. If the optional checkSum field is being utilized, it is then also calculated and inserted.

Note: The acceptance case: the exceptions cover cmdRequest, cmdResponse, testPort, authUnixTime, authDigest and checkSum. Reserved fields are still zeroed (RFC9946-6.1-4); the Server echoes keyId (ADR 0009).

Proving tests:

RFC9946-6.2.2-4

Tested

To ensure that a server’s local firewall will successfully allow packets received for the new ephemeral port number, the server SHALL immediately send a Null Request with the corresponding values including the source and destination IP addresses and port numbers.

Proving tests:

RFC9946-6.2.2-5

Tested

The source port SHALL be the new ephemeral port.

Proving tests:

RFC9946-6.2.2-6

Tested

If received, the client SHALL follow the Message Verification Procedure listed in Section 6.2, Paragraph 2.

Note: The Null Request; there is no response to it.

Proving tests:

RFC9946-6.2.2-7

Tested

The UDP PDU format layout SHALL be as follows (big-endian AB):

Note: The Null Request layout (Figure 4) follows.

Proving tests:

RFC9946-6.2.2-8

Tested

If a Test Activation Request is not subsequently received from the client on the new ephemeral port number before the watchdog timer expires, the server SHALL close the socket and deallocate the associated resources.

Proving tests:

RFC9946-6.2.2-9

Tested

The Null Request message PDU SHALL be organized as follows:

Note: Figure 5’s struct controlHdrNR follows.

Proving tests:

RFC9946-6.2.2-L1

Tested

The watchdog timer is set to the same value as on the client side (see Section 6)

Note: The RFC’s sentence has no final period. Timeouts per ADR 0002.

Proving tests:

RFC9946-6.2.2-L2

Tested

Note that there is no response to a Null Request.

Proving tests:

RFC9946-6.2.2-L3

Tested

The authentication and checkSum fields follow the same methodology as with the Setup Request and Response.

Note: Null Request.

Proving tests:

RFC9946-6.2.2-L4

Tested

Set to CHNR_CREQ_NULLREQ to indicate a Null Request message.

Note: Null Request cmdRequest field.

Proving tests:

RFC9946-6.2.2-L5

Tested

Set to CHNR_CRSP_NONE.

Note: Null Request cmdResponse field.

Proving tests:

RFC9946-6.3-1

Tested

When the client receives the Test Setup Response message, it SHALL first follow the Message Verification Procedure listed in Section 6.2, Paragraph 2.

Conformance Records: setup-response-from-another-address, unverified-setup-rejection-hint.

Proving tests:

RFC9946-6.3-2

Tested

The client SHALL then proceed to evaluate the other fields in the protocol, beginning with the protocol version, PDU ID (to validate the type of message), and cmdRequest for the role of the message, which MUST be Test Setup Response, CHSR_CREQ_SETUPRSP, as indicated by Figure 3.

Note: How the Client checks protocolVer is unspecified; record setup-response-protocol-version.

Conformance Records: setup-response-protocol-version.

Proving tests:

RFC9946-6.3-3

Tested

If the cmdResponse value indicates an error (values greater than CHSR_CRSP_ACKOK), the client SHALL display/report a relevant message to the user or management process and exit.

Conformance Records: undefined-setup-cmd-response.

Proving tests:

RFC9946-6.3-4

Tested

If the client receives a Command Response code that is not equal to one of the codes defined, the client MUST terminate the connection and terminate operation of the current Setup Request.

Note: An undefined code above the defined range also matches RFC9946-6.3-3; CHSR_CRSP_NONE (0) is defined but neither error nor success.

Conformance Records: undefined-setup-cmd-response.

Proving tests:

RFC9946-6.3-5

Tested

If the Command Response code value indicates success (CHSR_CRSP_ACKOK), the client SHALL compose a Test Activation Request with all the test parameters it desires, such as the test direction, the test duration, etc., as described below.

Proving tests:

RFC 9946 §7: Test Activation Request and Response

The 44 Requirements of RFC 9946 §7, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested44

RFC9946-7.1-1

Tested

Upon a successful setup exchange, the client SHALL compose and send the Test Activation Request to the UDP port number the server communicated in the Test Setup Response (the new ephemeral port, and not the standard UDPSTP port).

Conformance Records: setup-response-from-another-address.

Proving tests:

RFC9946-7.1-2

Tested

This specification does not provide an ECN-capable transport; therefore, the sender SHALL set the ECN field to not_ECT.

Note: dscpEcn field. What a Server does with ECT bits in a request is unspecified; record dscp-from-an-allowed-set-never-ect.

Conformance Records: dscp-from-an-allowed-set-never-ect.

Proving tests:

RFC9946-7.1-3

Tested

Because a value of zero is a valid fixed or starting sending rate index, the field SHALL be set to its maximum (CHTA_SRIDX_DEF) when requesting the default behavior of the server (starting with the selected load rate adjustment algorithm at its minimum/zero index).

Note: srIndexConf field.

Conformance Records: sr-index-def-ignores-isstart.

Proving tests:

RFC9946-7.1-4

Tested

This SHALL be equivalent to setting srIndexConf to zero and setting the CHTA_SRIDX_ISSTART bit.

Note: srIndexConf field.

Conformance Records: sr-index-def-ignores-isstart.

Proving tests:

RFC9946-7.1-5

Tested

Used by the server in a Test Activation Response for an upstream test to communicate the (initial) Load PDU transmission parameters the client SHALL use.

Note: srStruct field. Sizes are IPv4-based and an IPv6 Load Sender subtracts 20 bytes (ADR 0004); the interpretation belongs on the Section 8.1 entry.

Conformance Records: ipv4-based-sending-rate-structure-sizes.

Proving tests:

RFC9946-7.1-6

Tested

For a Test Activation Request or a downstream test, this structure SHALL be zeroed.

Note: srStruct field.

Proving tests:

RFC9946-7.1-7

Tested

The Test Activation Request/Response message PDU (as well as the included Sending Rate structure) SHALL be organized as follows:

Note: Figure 7’s struct sendingRate and struct controlHdrTA follow.

Proving tests:

RFC9946-7.1-L1

Tested

The authentication and checkSum fields follow the same methodology as with the Setup Request and Response.

Note: Test Activation PDU.

Proving tests:

RFC9946-7.1-L2

Tested

Set to CHTA_CREQ_TESTACTUS to indicate an upstream test activation or alternatively to CHTA_CREQ_TESTACTDS to indicate a downstream test activation.

Note: cmdRequest field.

Proving tests:

RFC9946-7.1-L3

Tested

Note that CHTA_CREQ_NONE remains unused.

Note: cmdRequest field.

Proving tests:

RFC9946-7.1-L4

Tested

Boolean, default True (if False, use RTT=round-trip delay variation in the load rate adjustment algorithm; if True, use EnableIPDV, which uses one-way delay variation for the load rate adjustment algorithm).

Note: useOwDelVar field. The default differs from OB-UDPST and TR-471; record use-ow-del-var-defaults-to-true.

Conformance Records: use-ow-del-var-defaults-to-true.

Proving tests:

RFC9946-7.1-L5

Tested

When True, only loss counts toward received packet sequence number errors.

Note: ignoreOooDup field.

Proving tests:

RFC9946-7.1-L6

Tested

When False, loss, out-of-order, and duplicate totals are all counted as sequence number errors.

Note: ignoreOooDup field.

Proving tests:

RFC9946-7.1-L7

Tested

Default is True (see also Table 3 of [TR-471]).

Note: ignoreOooDup field.

Proving tests:

RFC9946-7.1-L8

Tested

A 100 us granularity is recommended for optimal rate selection.

Note: txInterval fields; the Sending Rate Table uses multiples of 100 us.

Proving tests:

RFC9946-7.1-L9

Tested

A four-octet field indicating the size of a single Load PDU to be sent at the end of the txInterval2 send sequence, even when udpPayload2 or burstSize2 are zero and result in no transmission of their own.

Note: udpAddon2 field.

Proving tests:

RFC9946-7.1-L10

Tested

Trials with subIntPeriod in a range of 100 to 10000 ms resulted in a default value of 1000 ms.

Note: subIntPeriod field.

Proving tests:

RFC9946-7.2-1

Tested

When the server replies to the Test Activation Request message, the Test Activation Response PDU is structured identically to the Request PDU and SHALL retain the original values received in it unless they are explicitly coerced to a server-acceptable value.

Note: Reserved fields are still zeroed (RFC9946-6.1-4), although OB-UDPST carries data in reserved2 and reserved4. Coercions are reported at both ends.

Proving tests:

RFC9946-7.2-2

Tested

When the server receives the Test Activation Request message, it SHALL first follow the Message Verification Procedure listed in Section 6.2, Paragraph 2.

Conformance Records: activation-auth-matches-setup.

Proving tests:

RFC9946-7.2-L1

Tested

After the server receives the Test Activation Request on the new connection, it chooses to accept, ignore, or modify any of the test parameters.

Proving tests:

RFC9946-7.2.1-1

Tested

When evaluating the Test Activation Request, the server MAY allow the client to specify its own fixed or starting send rate via srIndexConf.

Note: Fixed-Rate Tests only with an Operator Key; record fixed-rate-needs-operator-key.

Proving tests:

RFC9946-7.2.1-2

Tested

Alternatively, the server MAY enforce a maximum limit of the fixed or starting send rate, which the client can successfully request.

Conformance Records: consumer-start-row-capped.

Proving tests:

RFC9946-7.2.1-3

Tested

If the client’s Test Activation Request exceeds the server’s configured maximum, the server MUST either reject the request or coerce the value to the configured maximum bit rate, and communicate that maximum to the client in the Test Activation Response.

Note: srIndexConf is a row index, not a bit rate; records sr-index-conf-beyond-the-top-row-coerced and row-above-max-bandwidth-coerced.

Conformance Records: sr-index-conf-beyond-the-top-row-coerced, row-above-max-bandwidth-coerced, consumer-start-row-capped.

Proving tests:

RFC9946-7.2.1-4

Tested

In these cases, the Command Response value SHALL be CHTA_CRSP_ACKOK.

Note: Coercion of the parameters listed before it (RFC9946-7.2.1-L1); the list names useOwDelayVar (meaning useOwDelVar) and highSpeedDelta twice.

Conformance Records: row-above-max-bandwidth-coerced.

Proving tests:

RFC9946-7.2.1-5

Tested

If the authDigest is valid, a Test Activation Response SHALL be sent back to the client with a corresponding Command Response value indicating the reason for the rejection.

Note: The bullet under “If the Test Activation Request must be rejected (due to the Command Response value being CHTA_CRSP_BADPARAM), and”.

Proving tests:

RFC9946-7.2.1-6

Tested

If the authDigest is invalid, the Test Activation Request SHOULD fail silently.

Note: The RFC permits server administrators to send an Activation Response anyway, for operations and troubleshooting. Troubleshooting Mode answers Setup Requests only: an Activation Request that fails verification stays silent even then.

Conformance Records: activation-auth-matches-setup.

Proving tests:

RFC9946-7.2.1-7

Tested

The additional circumstances when a server SHALL NOT communicate the appropriate Command Response code for an error condition (fail silently) are when: * the Test Activation Request PDU size is not equal to the ‘struct controlHdrTA’ size shown in Figure 7, * the PDU ID is not 0xACE2 (Test Activation PDU), or * a directed attack has been detected.

Note: Attack detection is out of scope per the RFC, so the third case is not testable.

Proving tests:

RFC9946-7.2.1-L1

Tested

Other parameters where the server has the OPTION to coerce the client to use values other than those in the Test Activation Request are (grouped by role): * Load rate adjustment algorithm: lowThresh, upperThresh, useOwDelayVar, highSpeedDelta, slowAdjThresh, seqErrThresh, highSpeedDelta, ignoreOooDup, and rateAdjAlgo * Test duration/intervals: trialInt, testIntTime, and subIntPeriod * Packet marking: dscpEcn

Note: “OPTION” is not a BCP 14 keyword.

Conformance Records: parameter-ranges-follow-y1540, windows-dscp-coerced-to-0.

Proving tests:

RFC9946-7.2.1-L2

Tested

Note that the server also has the option of completely rejecting the request and sending back an appropriate cmdResponse field value (currently only CHTA_CRSP_BADPARAM; see Section 12.3.9).

Conformance Records: server-limits-refuse-with-connfail, test-has-one-direction.

Proving tests:

RFC9946-7.2.1-L3

Tested

Whether this error response is sent or not depends on the security mode of operation and the outcome of authDigest validation.

Proving tests:

RFC9946-7.2.1-L4

Tested

The exception is for operations support: server administrators are permitted to send an Activation Response to support operations and troubleshooting.

Note: Troubleshooting Mode.

Conformance Records: troubleshooting-replies-rate-limited.

Proving tests:

RFC9946-7.2.2-1

Tested

When the server sends the Test Activation Response, it SHALL set the cmdResponse field to CHTA_CRSP_ACKOK (see Section 12.3.9).

Proving tests:

RFC9946-7.2.2-2

Tested

If the client has requested an upstream test, the server SHALL: * include the transmission parameters from the first row of the Sending Rate Table in the Sending Rate structure (if requested by setting srIndexConf to a value of CHTA_SRIDX_DEF), or * include the transmission parameters from the designated Configured Sending Rate Table index (srIndexConf) of the Sending Rate Table where, if CHTA_SRIDX_ISSTART is set in modifierBitmap, this will be used as the starting rate for the load rate adjustment algorithm; else, it will be considered a fixed-rate test.

Proving tests:

RFC9946-7.2.2-3

Tested

When generating the Test Activation Response (acceptance) for a downstream test, the server SHALL set all octets of the Sending Rate structure to zero.

Proving tests:

RFC9946-7.2.2-4

Tested

In the case of an upstream test, the server SHALL prepare to use a single timer to send Status PDUs at the specified interval.

Proving tests:

RFC9946-7.2.2-5

Tested

For a downstream test, the server SHALL prepare to utilize dual timers to send Load PDUs based on: * the transmission parameters directly from the first row of the Sending Rate Table (if requested by srIndexConf having been set to CHTA_SRIDX_DEF), or * the transmission parameters from the designated Configured Sending Rate Table index (srIndexConf) of the Sending Rate Table where, if CHTA_SRIDX_ISSTART is set in modifierBitmap, this will be used as the starting rate for the load rate adjustment algorithm; else, it will be considered a fixed-rate test.

Proving tests:

RFC9946-7.2.2-6

Tested

The server SHALL then send the Test Activation Response back to the client, update the watchdog timer with a new timeout value, and set a test duration timer to eventually stop the test.

Note: The new watchdog timeout is unspecified; timeouts per ADR 0002.

Proving tests:

RFC9946-7.3-1

Tested

When the client receives the Test Activation Response message, it SHALL first follow the Message Verification Procedure listed in Section 6.2, Paragraph 2.

Proving tests:

RFC9946-7.3-2

Tested

If the client receives a Test Activation cmdResponse field value that indicates an error, the client SHALL display/report a relevant message to the user or measurement system and exit.

Proving tests:

RFC9946-7.3-3

Tested

If the client receives a Test Activation cmdResponse field value that is not equal to one of the codes defined in Section 12.3.9, the client MUST terminate the connection and terminate operation of the current setup procedure.

Proving tests:

RFC9946-7.3-4

Tested

If the client receives a Test Activation Command Response value that indicates success (e.g., CHTA_CRSP_ACKOK; see Section 12.3.9), the client SHALL update its configuration to use any test parameters modified by the server.

Note: The RFC continues: if the coerced parameters are not acceptable to the client, the client ends the test.

Proving tests:

RFC9946-7.3-5

Tested

To finalize an accepted test activation, the client SHALL prepare its connection for either an upstream test with dual timers set to send Load PDUs (based on the starting transmission parameters sent by the server) OR a downstream test with a single timer to send Status PDUs at the specified interval.

Proving tests:

RFC9946-7.3-6

Tested

Then, the client SHALL stop the test initiation timer and start a watchdog timer to detect if the server goes quiet.

Proving tests:

RFC9946-7.3-L1

Tested

If the setup parameters coerced by the server are not acceptable to the client, the client ends the test.

Conformance Records: multi-connection-parameters-differ, test-interval-over-60-s, client-refuses-a-changed-dscp.

Proving tests:

RFC 9946 §8: Test Load Stream Transmission and Measurement Status Feedback Messages

The 57 Requirements of RFC 9946 §8, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested55
Deviation2

RFC9946-8.1-1

Tested

When the server is sending Status Feedback messages, they will also contain the latest transmission parameters from the Sending Rate Table that the client SHALL use.

Note: srStruct in the Status PDU. Sizes are IPv4-based and an IPv6 Load Sender subtracts 20 bytes (ADR 0004).

Conformance Records: ipv4-based-sending-rate-structure-sizes.

Proving tests:

RFC9946-8.1-2

Tested

When a Load PDU is received, the receiver SHALL do the following: 1. Verify that the size of the message is greater than or equal to the ‘struct loadHdr’ size shown in Figure 9. 2. Validate the checksum for the Load PDU header portion of the total message (as described in Section 5.6) if the optional checkSum field is being utilized. 3. Confirm that the PDU ID is 0xBEEF (Load PDU).

Proving tests:

RFC9946-8.1-3

Tested

If any of the above checks fail, the message SHALL be considered invalid.

Conformance Records: undefined-flags-are-invalid.

Proving tests:

RFC9946-8.1-4

Tested

The watchdog timer at the receiver SHALL be reset each time a valid Load PDU is received (which includes verification of the checkSum, if in use).

Note: Timeouts per ADR 0002.

Conformance Records: receive-timeout-is-final.

Proving tests:

RFC9946-8.1-5

Tested

When the server is sending Load PDUs in the role of sender, it SHALL use the transmission parameters directly from the Sending Rate Table via the index that is currently selected (which was indirectly based on the feedback in its received Status Feedback messages).

Proving tests:

RFC9946-8.1-6

Deviation

However, when the client is sending Load PDUs in the role of sender, it SHALL use the discrete transmission parameters that were communicated by the server in its periodic Status Feedback messages (and not referencing a Sending Rate Table directly).

Note: srStruct in the Status PDU.

Recorded in sending-rate-structure-sanity-limits, client-keeps-to-its-max-bandwidth.

Conformance Records: ipv4-based-sending-rate-structure-sizes, random-size-flag, sending-rate-structure-only-from-test-status-pdus.

Proving tests:

RFC9946-8.1-7

Tested

The threshold of consecutive feedback intervals SHALL be configurable with a default of 3 intervals.

Note: Fast Mode of algorithm B; slowAdjThresh field (Section 7.1).

Proving tests:

RFC9946-8.1-8

Tested

An OPTIONAL load rate adjustment algorithm (designated C) has been defined in [TR-471].

Note: rateAdjAlgo C (Section 12.3.8).

Proving tests:

RFC9946-8.1-9

Tested

On the other hand, the test configuration MAY use a fixed sending rate requested by the client, using the field srIndexConf.

Note: Fixed-Rate Tests.

Proving tests:

RFC9946-8.1-10

Tested

The client MAY communicate the desired fixed rate in its Test Activation Request.

Note: srIndexConf without CHTA_SRIDX_ISSTART.

Proving tests:

RFC9946-8.1-11

Tested

The UDP PDU format layout SHALL be as follows (big-endian AB):

Note: Figure 8, the Load PDU layout, follows.

Proving tests:

RFC9946-8.1-12

Tested

All outgoing Load or Status PDUs SHALL continue to assert this indication until traffic is received again, or the test is terminated.

Note: rxStopped field.

Conformance Records: receive-timeout-is-final.

Proving tests:

RFC9946-8.1-13

Tested

The Load PDU SHALL be organized as follows (followed by any payload content):

Note: Figure 9’s struct loadHdr follows.

Proving tests:

RFC9946-8.1-L1

Tested

The default algorithm (B; see [Y.1540]) has three paths to its decision on the next sending rate: 1. When there are no impairments present (no sequence errors and low delay variation), resulting in a sending rate increase. 2. When there are low impairments present (no sequence errors but higher levels of delay variation), the same sending rate is maintained. 3. When the impairment levels are above the thresholds set for this purpose and “congestion” is inferred, resulting in a sending rate decrease.

Note: Load Rate Adjustment Algorithm; its requirements are in Section 5.1.

Proving tests:

RFC9946-8.1-L2

Tested

Consecutive feedback intervals that have a supra- threshold count of sequence number anomalies and/or contain an upper delay variation threshold exception in all of the consecutive intervals are sufficient to declare “congestion” within a test.

Note: Fast Mode of algorithm B.

Proving tests:

RFC9946-8.1-L3

Tested

The single-step mode continues after the first inference of “congestion” from measured impairments.

Note: Algorithm B; algorithm C may make a Fast-Mode Retry.

Proving tests:

RFC9946-8.1-L4

Tested

Boolean, 0 or 1, used to indicate to the remote endpoint that local receive traffic (either Load or Status PDUs) has stopped.

Note: rxStopped field.

Proving tests:

RFC9946-8.1-L5

Tested

The time threshold to trigger this condition is expected to be a reasonable fraction of the watchdog timeout (a default of one second is recommended).

Note: rxStopped field; record rx-stopped-from-half-the-timeout and ADR 0002.

Conformance Records: rx-stopped-from-half-the-timeout.

Proving tests:

RFC9946-8.1-L6

Tested

A four-octet field indicating the Load PDU sequence number (starting at 1).

Note: lpduSeqNo field.

Proving tests:

RFC9946-8.1-L7

Tested

This field allows the Load PDU receiver to maintain accurate receive statistics if utilizing receive truncation (only requesting the Load PDU message header octets from the protocol stack).

Note: udpPayload field.

Conformance Records: received-bytes-from-datagram-length.

Proving tests:

RFC9946-8.1-L8

Tested

This is determined by the Status PDU sequence number (spduSeqNo) in the most recently received Status PDU.

Note: spduSeqErr field.

Conformance Records: spdu-seq-err-is-the-gap-before-the-latest.

Proving tests:

RFC9946-8.1-L9

Tested

Two four-octet fields containing a copy of the most recent spduTime_sec/spduTime_nsec from the last Status PDU received.

Note: spduTime_sec/spduTime_nsec fields of the Load PDU.

Proving tests:

RFC9946-8.1-L10

Tested

Two four-octet fields containing the local send time of the Load PDU.

Note: lpduTime_sec/lpduTime_nsec fields.

Conformance Records: lpdu-time-per-burst.

Proving tests:

RFC9946-8.1-L11

Tested

On the Load PDU sender, it is the number of ms from reception of the most recent Status PDU (when the latest spduTime_sec/spduTime_nsec was obtained) to the transmission of the Load PDU (where the previously obtained spduTime_sec/spduTime_nsec is returned).

Note: rttRespDelay field.

Proving tests:

RFC9946-8.1-L12

Tested

When the Load PDU receiver is calculating RTT, by subtracting the copied Status PDU send time (in the Load PDU) from the local Load PDU receive time, this value is subtracted from the raw RTT to correct for any response delay due to Load PDU scheduling.

Note: rttRespDelay field.

Conformance Records: rtt-sample-rules.

Proving tests:

RFC9946-8.1-L13

Tested

The checksum does not cover the payload content.

Note: checkSum field of the Load PDU.

Proving tests:

RFC9946-8.1-L14

Tested

The calculation is done as the very last step of building the PDU header, with the checkSum field set to zero.

Note: checkSum field of the Load PDU.

Proving tests:

RFC9946-8.1-L15

Tested

All zeroes, all ones, or a pseudorandom binary sequence.

Note: Payload Content; randomized with the Test Activation modifier bit 0x02 (Section 12.3.7).

Proving tests:

RFC9946-8.2-1

Tested

The Load PDU receiver SHALL send a Status PDU to the sender during a test at the configured feedback interval, after at least one Load PDU has been received (when there is something to provide status on).

Note: The “feedback interval” is the Trial Interval, trialInt.

Conformance Records: status-pdu-grid-from-first-load-pdu.

Proving tests:

RFC9946-8.2-2

Tested

In these cases, the Status PDU SHALL NOT be sent.

Note: Before the first Load PDU arrives.

Proving tests:

RFC9946-8.2-3

Tested

When the Load PDU sender receives a Status PDU message, it SHALL first follow the Message Verification Procedure listed in Section 6.2, Paragraph 2.

Proving tests:

RFC9946-8.2-4

Tested

The watchdog timer at the Load PDU sender SHALL be reset each time a valid Status PDU is received (which includes verification of the checkSum and/or authDigest, if in use).

Note: Timeouts per ADR 0002.

Conformance Records: stale-status-pdus-count-only-as-heard.

Proving tests:

RFC9946-8.2-5

Tested

The UDP PDU format layout SHALL be as follows (big-endian AB):

Note: Figure 10, the Status PDU layout, follows.

Proving tests:

RFC9946-8.2-6

Tested

Both sets of statistics SHALL always be populated by the Load PDU receiver, regardless of role (client or server).

Note: sisSav and the Trial Interval fields.

Proving tests:

RFC9946-8.2-7

Tested

This minimum SHALL be kept current (and continuously updated) via each Load PDU received with an updated spduTime_sec/spduTime_nsec.

Note: rttMinimum field.

Proving tests:

RFC9946-8.2-8

Deviation

This value MUST be positive.

Note: rttMinimum field.

Recorded in rtt-minimum-may-be-zero.

RFC9946-8.2-9

Tested

Before an initial value can be established, and because zero is itself valid, it SHALL be set to STATUS_NODEL when communicated in the Status PDU.

Note: rttMinimum field.

Proving tests:

RFC9946-8.2-10

Tested

When a new value is unavailable (possibly due to packet loss), and because zero is itself valid, it SHALL be set to STATUS_NODEL when communicated in the Status PDU.

Note: rttVarSample field.

Proving tests:

RFC9946-8.2-11

Tested

The Status Feedback message PDU (as well as the included Sub-Interval statistics structure) SHALL be organized as follows:

Note: Figure 11’s struct subIntStats and struct statusHdr follow.

Proving tests:

RFC9946-8.2-L1

Tested

The authentication and checkSum fields follow the same methodology as with the Setup Request and Response.

Note: Status PDU; Section 6.1.

Proving tests:

RFC9946-8.2-L2

Tested

A four-octet field containing the Status PDU sequence number (starting at 1).

Note: spduSeqNo field.

Conformance Records: stale-status-pdus-count-only-as-heard.

Proving tests:

RFC9946-8.2-L3

Tested

A four-octet field containing the Sub-Interval sequence number (starting at 1) that corresponds to the statistics provided in sisSav, for the last saved (completed) Sub-Interval.

Note: subIntSeqNo field.

Conformance Records: sub-interval-boundaries.

Proving tests:

RFC9946-8.2-L4

Tested

seqErrOoo and seqErrDup are realized by comparing sequence numbers.

Proving tests:

RFC9946-8.2-L5

Tested

A lookback list of the last n sequence numbers received is used as the basis.

Note: seqErrOoo and seqErrDup.

Proving tests:

RFC9946-8.2-L6

Tested

Each Load PDU sequence number is checked against this lookback.

Note: seqErrOoo and seqErrDup.

Proving tests:

RFC9946-8.2-L7

Tested

The number n may depend on the implementation and on typical characteristics of environments, where UDPSTP is deployed (like mobile networks or Wi-Fi).

Note: Lookback length.

Proving tests:

RFC9946-8.2-L8

Tested

Currently, a default sequence number interval of n=32 has been chosen.

Note: Lookback length.

Proving tests:

RFC9946-8.2-L9

Tested

Specifically for seqErrOoo, each successively received higher seqno sets the next-expected seqno to seqno+1, and anything below that is considered out of order (i.e., delayed).

Conformance Records: late-load-pdu-decrement-in-its-own-interval, lpdu-seq-no-modulo-2-32.

Proving tests:

RFC9946-8.2-L10

Tested

For each Load PDU received, the send time (lpduTime_sec/lpduTime_nsec) is subtracted from the local receive time, which is then normalized by subtracting the current clockDeltaMin.

Note: delayVarMin/delayVarMax/delayVarSum/delayVarCnt fields.

Conformance Records: ms-rounding-half-up.

Proving tests:

RFC9946-8.2-L11

Tested

Equivalent to the sum of each deltaTime (although in ms) sent in each Status PDU during the test.

Note: accumTime field.

Conformance Records: sub-interval-boundaries.

Proving tests:

RFC9946-8.2-L12

Tested

Obtained by subtracting the send time of each Load PDU (lpduTime_sec/ lpduTime_nsec) from the local time that it was received.

Note: clockDeltaMin field.

Proving tests:

RFC9946-8.2-L13

Tested

This value is initialized with the first Load PDU received and is updated with each subsequent one to maintain a current (and continuously updated) minimum.

Note: clockDeltaMin field.

Proving tests:

RFC9946-8.2-L14

Tested

Otherwise, this value may be negative but still valid for one-way delay variation measurements for the default test duration (default is 10 seconds).

Note: clockDeltaMin field; the RFC gives no encoding for a negative value.

Conformance Records: clock-delta-min-encoding.

Proving tests:

RFC9946-8.2-L15

Tested

RTT is obtained by subtracting the copied spduTime_sec/ spduTime_nsec in the received Load PDU from the local time at which it was received.

Note: rttMinimum field; adjusted by rttRespDelay (RFC9946-8.1-L12).

Conformance Records: rtt-sample-rules.

Proving tests:

RFC9946-8.2-L16

Tested

RTT delay variation is obtained by subtracting the current (and continuously updated) “adjusted” RTT minimum, communicated as rttMinimum (in Status PDU), from each “adjusted” RTT measurement (which is itself obtained by subtracting the copied spduTime_sec/spduTime_nsec in the received Load PDU from the local time at which it was received).

Note: rttVarSample field.

Proving tests:

RFC9946-8.2-L17

Tested

Note that while one-way delay variation is measured for every Load PDU received, RTT delay variation is only sampled via the Status PDU sent and the very next Load PDU received with the corresponding updated spduTime_sec/spduTime_nsec.

Note: rttVarSample field.

Proving tests:

RFC9946-8.2-L18

Tested

Boolean, 0 or 1, indicating that the clockDeltaMin and/or rttMinimum (in Status PDU), as measured by the Load PDU receiver, has been updated.

Note: delayMinUpd field.

Proving tests:

RFC 9946 §9: Stopping a Test

The 12 Requirements of RFC 9946 §9, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested12

RFC9946-9-1

Tested

When the test duration timer (testIntTime) on the server expires, it SHALL set the local connection test action to TEST_ACT_STOP1 (phase 1 of graceful termination).

Conformance Records: server-test-interval-timer-runs-half-a-second-over.

Proving tests:

RFC9946-9-2

Tested

Upon transmission of the next Load or Status PDUs, the server SHALL set the local connection test action to TEST_ACT_STOP2 (phase 2 of graceful termination) and mark any outgoing PDUs with a testAction value of TEST_ACT_STOP2.

Conformance Records: stop-confirmation-waits.

Proving tests:

RFC9946-9-3

Tested

While in this state, the server MAY reduce any Load PDU bursts to a size of one.

Conformance Records: stop-bursts-of-one-without-add-on.

Proving tests:

RFC9946-9-4

Tested

When the client receives a Load or Status PDU with the TEST_ACT_STOP2 indication, it SHALL finalize testing, display the test results, and mark its local connection with a test action of TEST_ACT_STOP2 (so that any PDUs subsequently received can be ignored).

Conformance Records: stop-test-action-per-role, stop-ends-counting.

Proving tests:

RFC9946-9-5

Tested

With the test action of the client’s connection set to TEST_ACT_STOP2, the very next expiry of a send timer, for either a Load or a Status PDU, SHALL result in it and any subsequent PDUs being sent with a testAction value of TEST_ACT_STOP2 (as confirmation to the server).

Proving tests:

RFC9946-9-6

Tested

While in this state, the client MAY reduce any Load PDU bursts to a size of one.

Conformance Records: stop-bursts-of-one-without-add-on.

Proving tests:

RFC9946-9-7

Tested

The client SHALL then schedule an immediate end time for the connection.

Proving tests:

RFC9946-9-8

Tested

When the server receives the TEST_ACT_STOP2 confirmation in the Load or Status PDU, the server SHALL schedule an immediate end time for the connection that closes the socket and deallocates the associated resources.

Conformance Records: stop-test-action-per-role.

Proving tests:

RFC9946-9-9

Tested

In a non-graceful test stop due to path failure, the watchdog timeouts at each endpoint will expire (sometimes at one endpoint first); notifications in logs, STDOUT, and/or formatted output SHALL be made; and the endpoint SHALL schedule an immediate end time for the connection.

Note: Timeouts per ADR 0002.

Proving tests:

RFC9946-9-10

Tested

If an attacker clears the TEST_ACT_STOP2 indication, then the configured test duration timer (testIntTime) at the server and client SHALL take precedence and the endpoint SHALL schedule an immediate end time for the connection.

Proving tests:

RFC9946-9-L1

Tested

This is simply a non-reversible state awaiting the next message(s) to be sent from the server.

Note: TEST_ACT_STOP1.

Proving tests:

RFC9946-9-L2

Tested

During this time, any received Load or Status PDUs are processed normally.

Note: TEST_ACT_STOP1.

Conformance Records: stop-status-pdus-are-not-classified.

Proving tests:

RFC 9946 §10: Operational Considerations for the Measurement Method

The 5 Requirements of RFC 9946 §10, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested1
Deviation1
Operational3

RFC9946-10-1

Tested

The nominal duration of a measurement interval at the Destination, parameter testIntTime, MUST be constrained in a production network, since this is an active test method and it will likely cause congestion on the Src to Dst host path during a test.

Note: The Server bounds testIntTime; a Consumer’s Test Interval is at most 60 s.

Proving tests:

RFC9946-10-2

Operational

It is RECOMMENDED to locate test endpoints as close to the intended measured link(s) as practical.

Note: Where the operator places Clients and Servers.

RFC9946-10-3

Operational

The testing operator MUST set a value for the MaxHops parameter, based on the expected path length.

Note: The operator sets it; udpstp provides the MaxHops setting (max_hops).

Proving tests:

RFC9946-10-4

Deviation

The number of concurrent, independent tests of a path SHALL be limited to one.

Note: Tests per source; see also Section 4.2.

Recorded in tests-per-source-above-one.

Proving tests:

RFC9946-10-5

Operational

It is RECOMMENDED to discontinue non-measurement traffic that shares a subscriber’s dedicated resources while testing: measurements may not be accurate, and throughput of competing elastic traffic may be greatly reduced.

Note: What the Consumer does while testing.

RFC 9946 §11: Security Considerations

The 8 Requirements of RFC 9946 §11, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested6
Deviation1
Operational1

RFC9946-11-1

Operational

Cooperating client and server hosts and agreements to test the path between the hosts are REQUIRED.

Note: Agreements between the parties; authentication (Section 5.3) is one way to assure them.

RFC9946-11-2

Tested

It is REQUIRED to have a user client-initiated setup handshake between cooperating hosts that allows firewalls to control inbound unsolicited UDP traffic that goes to either a control port or ephemeral ports that are only created as needed.

Note: Test Setup exchange (Section 6); Ephemeral Ports opened per Test Connection.

Proving tests:

RFC9946-11-3

Tested

Client-server authentication and integrity protection for feedback messages conveying measurements is RECOMMENDED.

Note: authMode 2 (Section 5.3.2).

Proving tests:

RFC9946-11-4

Tested

Hosts MUST limit the number of simultaneous tests to avoid resource exhaustion and inaccurate results.

Note: Server limits on Tests and Test Connections.

Conformance Records: per-test-cap-over-the-tests-life, admitted-rate-reserved-at-activation, max-test-share-of-the-rate-cap, unactivated-test-connection-share.

Proving tests:

RFC9946-11-5

Tested

Senders MUST be rate-limited.

Note: The Sending Rate Table bounds every rate.

Conformance Records: search-capped-at-stated-max-bandwidth, consumer-start-row-capped.

Proving tests:

RFC9946-11-6

Deviation

Testing with the Service Provider’s measurement hosts SHOULD be limited in frequency and/or overall volume of test traffic (for example, the range of test interval duration values should be limited).

Recorded in no-frequency-or-volume-quotas.

Proving tests:

RFC9946-11-L1

Tested

This can be accomplished using a pre-built table defining all the offered sending rates that will be supported.

Note: The Sending Rate Table.

Proving tests:

RFC9946-11-L2

Tested

Optionally, the server could utilize the maxBandwidth field (and the CHSR_USDIR_BIT bit) in the Setup Request from the client to limit the maximum that it will attempt to achieve.

Note: maxBandwidth field (Section 6.1).

Conformance Records: search-capped-at-stated-max-bandwidth.

Proving tests:

RFC 9946 §12: IANA Considerations

The 13 Requirements of RFC 9946 §12, each quoted from the RFC with its state and the tests that prove it. Conformance explains the states.

StateRequirements
Tested13

RFC9946-12.1-L1

Tested

IANA has registered the following service name in the “Service Name and Transport Protocol Port Number Registry”:

Note: Service Name and Transport Protocol Port Number Registry: udpstp, UDP port 24601, the Standard Port.

Proving tests:

RFC9946-12.2-L1

Tested

IANA has added the following KDF entry to the “KeyTable KDFs” registry (see <https://www.iana.org/assignments/keytable>):

Note: KeyTable KDFs registry: HMAC-SHA-256, the KDF of Section 5.4.1.

Proving tests:

RFC9946-12.3.1-L1

Tested

IANA has assigned values in the “PDU Identifier” registry as follows:

Note: PDU Identifier registry (pduId): 0xACE1 Test Setup, 0xACE2 Test Activation, 0xBEEF Load, 0xDEAD Null, 0xFEED Status Feedback; 0x0000 and 0xFFFF reserved.

Proving tests:

RFC9946-12.3.2-L1

Tested

IANA has assigned decimal value 20 in the “Protocol Version” registry as follows:

Note: Protocol Version registry (protocolVer): 20.

Proving tests:

RFC9946-12.3.3-L1

Tested

IANA has assigned bitmap values in the “Test Setup PDU Modifier Bitmap” registry as follows:

Note: Test Setup PDU Modifier Bitmap registry (modifierBitmap): 0x01 Jumbo datagram sizes above 1 Gbps, 0x02 Traditional MTU.

Proving tests:

RFC9946-12.3.4-L1

Tested

IANA has assigned decimal values in the “Test Setup PDU Authentication Mode” registry as follows:

Note: Test Setup PDU Authentication Mode registry (authMode): 0 not used, 1 the Control Phase, 2 the Control Phase and Status PDUs.

Proving tests:

RFC9946-12.3.5-L1

Tested

IANA has assigned decimal values in the “Test Setup PDU Command Response Field” registry as follows:

Note: Test Setup PDU Command Response Field registry (cmdResponse): 0 None, 1 Acknowledgment, 2 to 13 errors; 255 reserved.

Conformance Records: server-limits-refuse-with-connfail.

Proving tests:

RFC9946-12.3.5-L2

Tested

Note that value 4 is required for backward compatibility with previous experimental versions of software already in use.

Note: Test Setup cmdResponse 4, unexpected authentication in Setup Request.

Proving tests:

RFC9946-12.3.5-L3

Tested

Further, value 6 signals that a client erroneously used an authMode that hasn’t been standardized yet (i.e., authMode is greater than 1 or 2).

Note: Test Setup cmdResponse 6; “greater than 1 or 2” is garbled.

Proving tests:

RFC9946-12.3.6-L1

Tested

IANA has assigned decimal values in the “Test Activation PDU Command Request” registry as follows:

Note: Test Activation PDU Command Request registry (cmdRequest): 0 No Request, 1 upstream, 2 downstream; 255 reserved. The section says “Test Setup PDU layout” in error.

Proving tests:

RFC9946-12.3.7-L1

Tested

IANA has assigned bitmap values in the “Test Activation PDU Modifier Bitmap” registry as follows:

Note: Test Activation PDU Modifier Bitmap registry (modifierBitmap): 0x01 CHTA_SRIDX_ISSTART, 0x02 randomized UDP payload.

Proving tests:

RFC9946-12.3.8-L1

Tested

IANA has assigned capitalized alphabetic UTF-8 values, as well as the corresponding incremental numeric values, in the “Test Activation PDU Rate Adjustment Algo” registry as follows:

Note: Test Activation PDU Rate Adjustment Algo registry (rateAdjAlgo): B is 0, C is 1; A not used.

Proving tests:

RFC9946-12.3.9-L1

Tested

IANA has assigned decimal values in the “Test Activation PDU Command Response Field” registry as follows:

Note: Test Activation PDU Command Response Field registry (cmdResponse): 0 None, 1 CHTA_CRSP_ACKOK, 2 CHTA_CRSP_BADPARAM; 255 reserved.

Proving tests:

Deviations

A Deviation is a recorded departure from a Requirement, with its reason. udpstp has 17. Each says whether it applies by default, which setting controls it, and what would make it worth revisiting.

Non-conformances (9)

approximate-upstream-aggregation

In an Upstream Multi-Connection Test each Test Connection’s Load Receiver, at the Server, keeps its own Sub-Interval grid, which the Client can’t align. The Client sums the Test Connections’ Sub-Intervals by subIntSeqNo, so an aggregate Sub-Interval’s bits come from up to as many slightly different dt-long intervals as there are Test Connections, not from one contiguous interval. The results are marked approximate, with the largest offset between the Test Connections’ Sub-Intervals shown.

RFC: RFC 9097 §5.3
Requirements: RFC9097-5.3-L1
By default: on
Revisit: When a Server can be told a common Sub-Interval grid (such as a start time in the Activation Request), or reports per-Trial-Interval counts the Client could re-bin as it does Downstream.
References: docs/spec.md, docs/spec.md story 62

Proving tests:

client-keeps-to-its-max-bandwidth

A Client that stated a maxBandwidth keeps to it as Load Sender: a Sending Rate Structure above it, IP-layer with IPv4 headers as the rows are rated, is sent with its intervals lengthened by the ratio, rounded up, so its packet sizes and Bursts stay as the Server chose them; the results say so. RFC 9946 §8.1 says the Client SHALL use the parameters the Server communicates, but maxBandwidth is the most the Client wants to send (§6.1), and a Server needn’t honour it (§11 item 5 makes that optional). Lengthening intervals rather than shrinking Bursts or sizes keeps the packet sizes the Test measures with and works for a Burst of one.

RFC: RFC 9946 §6.1, §8.1, §11
Requirements: RFC9946-8.1-6
OB-UDPST: An OB-UDPST Client sends whatever the Server assigns; an OB-UDPST Server caps its rows at maxBandwidth only when built with RATE_LIMITING.
By default: on
Setting: --max-bandwidth
Revisit: If every Server honoured the stated maxBandwidth, or RFC 9946 said what a Client should do with a Sending Rate Structure above it.

Proving tests:

no-preamble-transmission

The Client can’t send a short preamble to wake on-demand access (such as a cellular radio) and leave it out of the results. RFC 9946 defines no preamble, and its search starting from row 0 only ramps up. A slow wake-up shows in the first Sub-Intervals, which a user can put in a Capacity Mode of their own (--first-mode-sub-intervals).

RFC: RFC 9097 §8.3
Requirements: RFC9097-8.3-4
OB-UDPST: OB-UDPST has none either; its JSON reports PreambleDuration 0.
By default: on
Revisit: If a user tests over on-demand access and the first Sub-Intervals mislead, or a later UDPSTP version defines a preamble.
References: docs/conformance/requirements/rfc9097-08.toml

No proving test.

unauthenticated-mode

A Server may serve unauthenticated Tests (authMode 0), and a Client may ask for one, only when each opts in. Such a Test has no authentication and no replay protection: requests in Unauthenticated Mode carry no digest, so they bypass the replay cache, and they spend no per-Source budget.

RFC: RFC 9946 §5.3, §6.1
Requirements: RFC9946-6.1-L11, RFC9946-5.3-1
OB-UDPST: OB-UDPST runs unauthenticated whenever the Server has no keys and the Client sends authMode 0, with no opt-in.
By default: off
Setting: --allow-unauthenticated, allow_unauthenticated in TOML (Server); --unauthenticated (Client)
Revisit: If the interop matrix no longer needs Unauthenticated Mode against OB-UDPST, or a lab use for it disappears.

Proving tests:

tests-per-source-above-one

A Server may allow more than one concurrent Test per source, for Clients behind carrier-grade NAT that share an address. RFC 9946 §4.2 limits a path to one.

RFC: RFC 9946 §4.2
Requirements: RFC9946-4.2-1, RFC9946-10-4, RFC9097-8.3-6
By default: off
Setting: --tests-per-source, tests_per_source in TOML
Revisit: If a per-path measure better than the source address appears, or CGNAT deployments don’t need it.

Proving tests:

authentic-setup-requests-left-unanswered

Some Setup Requests with a valid authDigest that are refused get no Test Setup Response: beyond a Source’s budget, all but the first refusal since the Source last spent a token; a copy of an admitted Setup Request (same address, mcIdent and mcIndex), which the network may have duplicated; and a new request while the replay cache is full. Answering each would let a key holder make the Server reflect a signed response per datagram, and a duplicate’s refusal would end the Test it belongs to.

RFC: RFC 9946 §6.2.1
Requirements: RFC9946-6.2.1-3
OB-UDPST: OB-UDPST answers every refused Setup Request; it has no per-Source budget or replay cache, and refuses a duplicate as a Test Connection it already has.
By default: on
Revisit: If a rate limit on signed rejections made answering every refused request safe, or a Client were seen to hang on a refusal it was never sent.
References: docs/spec.md

Proving tests:

operator-keys-exempt-from-500-ms-congestion-inference

A Test with an Operator Key keeps the slowAdjThresh and trialInt it asked for, even when inferring Congestion then takes longer than the 500 ms RFC 9946 §5.1 item 5 sets as its maximum. An Operator configures the measurement, for example to study slow paths.

RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-8
OB-UDPST: OB-UDPST applies no 500 ms limit to anyone.
By default: on
Revisit: If no Operator needs Congestion inferred more slowly, or RFC 9946 §5.1’s limit is clarified as applying to consumer Tests only.
References: docs/spec.md

Proving tests:

sending-rate-structure-sanity-limits

A Client zeroes each Sending Rate Structure field beyond OB-UDPST’s sanity limits, as an OB-UDPST Client does: an interval above 1 s, a size above 8972 bytes (the random-size flag masked off), a Burst above 100. A size above the largest the Test’s Sending Rate Table variant allows is zeroed too. §8.1 says the Client uses the parameters it receives and gives no limits; this keeps a Server from making it send absurd sizes or Bursts. Only the size limit rests on the RFC (§6.1’s packet sizes); the interval and Burst limits depart from §8.1.

RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-6
By default: on
Revisit: If a Server needed intervals above 1 s or Bursts above 100 datagrams, or RFC 9946 gave limits of its own.

Proving tests:

no-lost-status-backoff-upstream

Lost Status Backoff runs only in Downstream Tests, where the Server is the Load Sender. RFC 9097 §8.1, which RFC 9946 §5.1 imports, says “the offered load SHALL then be decreased” when Status PDUs stop arriving; in an Upstream Test nothing lowers it: the Client SHALL send the rates the Server communicates (RFC 9946 §8.1), and a lower rate from the Server could reach it only in the Status PDUs being lost. The Test relies on the 1 s Status PDU timeout instead, which stops the Client (ADR 0002).

RFC: RFC 9097 §8.1; RFC 9946 §4, §5.1, §8.1
Requirements: RFC9097-8.1-13
By default: on
Revisit: If the Client, as Load Sender, were allowed to lower its own rate when Status PDUs stop arriving, or Upstream Tests were seen to flood a path during feedback loss before the 1 s timeout.
References: ADR 0002

Proving tests:

Interop accommodations (1)

rtt-minimum-may-be-zero

rttMinimum may be 0 although §8.2 says it MUST be positive: an RTT below a millisecond, or one that rttRespDelay’s rounding brings to 0 (rtt-sample-rules), is reported as it is, as OB-UDPST reports it, rather than raised to 1 ms.

RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-8
OB-UDPST: OB-UDPST reports 0 too.
By default: on
Revisit: If a peer refuses a Status PDU whose rttMinimum is 0, or RFC 9946 errata say which to send.

Proving tests:

Platform gaps (1)

windows-dont-fragment-unset

On Windows test traffic leaves without DF, since no crate udpstp allows sets IP_DONTFRAGMENT without unsafe. The Client and Server warn at startup. Linux and macOS set it by default.

RFC: RFC 9946 §5
Requirements: RFC9946-5-1
By default: on
Revisit: An allowed crate sets DF on Windows without our own unsafe.
References: docs/technology.md

Proving tests:

Unmet SHOULDs (6)

rate-changes-when-a-status-pdu-arrives

The Load Sender changes its rate when a Status PDU arrives, as the algorithm decides, not at the next boundary of its own st: the Load Receiver’s Trial Intervals, which the Status PDUs close, and the Load Sender’s st are the same length but offset by the one-way delay. So one st can carry two rates, and its IP-Layer Sender Bit Rate is their mix.

RFC: RFC 9097 §7.3
Requirements: RFC9097-7.3-4
OB-UDPST: OB-UDPST changes the rate when a Status PDU arrives too.
By default: on
Revisit: If per-st Sender Bit Rates were reported (sender-bit-rate-per-sub-interval) and a clean rate per st mattered: defer each change to the next st boundary.

Proving tests:

compact-human-final-metrics

Human results show labelled final metrics rather than a second table repeating the Maximum IP-Layer Capacity. Phase and F appear in the Results Summary parameters, and each Capacity Mode retains its Maximum IP-Layer Capacity, Loss Ratio and RTT bounds. This makes the main result prominent on wide and narrow terminals. The Sub-Interval table and structured JSON retain their measurement data.

RFC: RFC 9097 §9
Requirements: RFC9097-9-2, RFC9097-9-3, RFC9097-9-4, RFC9097-9-5
By default: on
Revisit: If tabular final metrics become necessary for human comparison across Tests; machine consumers use JSON.

Proving tests:

sender-bit-rate-per-sub-interval

The IP-Layer Sender Bit Rate is reported per Sub-Interval, for the aggregate of the Test Connections, in the text and the JSON, not as RFC 9097 §9’s table of every st for each Test Connection and the aggregate: that table has 20 rows a second for each Test Connection at the default st of 50 ms. The per-st figures exist in the Data Phase’s events.

RFC: RFC 9097 §9
Requirements: RFC9097-9-11, RFC9097-9-12
By default: on
Revisit: If a user needs the per-st, per-Test-Connection rates, for example to study a sender’s burstiness: add them to the JSON first.

Proving tests:

no-frequency-or-volume-quotas

Quotas are off by default: unless the administrator sets one, the Server doesn’t limit how often, or how much, one Client tests, only how much runs at once (Tests per Source, Test Connections, the Admitted Rate Cap) and how long a Consumer’s Test Interval is (60 s), which is RFC 9946’s own example. Set, a Source’s or a Key Table row’s Quota of Tests and load bytes per Quota Period refills evenly; a new Test past it is refused with CONNFAIL at Setup, or with BADPARAM when it first activates after Tests set up alongside it used the Quota first. Operator Keys are exempt. Use is counted in memory, so a restart forgets it; a per-Source Quota treats everyone behind one carrier-grade NAT address as one Source, and an IPv6 Client can move between /64s.

RFC: RFC 9946 §11
Requirements: RFC9946-11-6
OB-UDPST: OB-UDPST has no such quotas.
By default: on
Setting: --quota-period, --source-tests-per-quota-period, --source-bytes-per-quota-period; tests_per_quota_period and bytes_per_quota_period on a key table row
Revisit: If a deployment serves Consumers whose data volumes are limited, set Quotas there; revisit the default if RFC 9946 §11 item 6 is read as requiring them on.
References: docs/spec.md

Proving tests:

row-zero-below-half-a-megabit

Row 0 offers about 0.1 Mbps (one 625-byte packet every 50 ms), not the 0.5 Mbps RFC 9097 §8.1 recommends; rows 1 to 1000 follow its 1 Mbps steps. The table follows OB-UDPST’s, whose row 0 is about 0.11 Mbps, so a search starts alike against either (row-zero-fixed-size).

RFC: RFC 9097 §8.1
Requirements: RFC9097-8.1-3
OB-UDPST: OB-UDPST’s row 0 is about 0.11 Mbps too, from a random-size add-on.
By default: on
Revisit: If a search’s first Trial Intervals at row 0 mattered, or matching OB-UDPST’s table stopped mattering.

Proving tests:

table-steps-above-1-gbps

Above the 1 Gbps row the Sending Rate Table follows OB-UDPST’s rows, so a search runs alike against either. Its steps are 100 Mbps to 10 Gbps with the default 1250-byte packets, as recommended, but 120 Mbps with Traditional MTU’s 1500-byte packets, and with Jumbo 100 Mbps only to 7.2 Gbps, then 720 Mbps (one more 9000-byte packet a Burst) to 72 Gbps, not 1 Gbps above 10 Gbps.

RFC: RFC 9097 §8.1
Requirements: RFC9097-8.1-4, RFC9097-8.1-5
OB-UDPST: OB-UDPST’s table has the same rows.
By default: on
Revisit: If matching OB-UDPST’s table stopped mattering, or searches above 7.2 Gbps needed finer steps than 720 Mbps.

Proving tests:

Interpretations

An Interpretation is a recorded reading of a Requirement that is ambiguous, silent on a case or in conflict with another, or a choice the RFC leaves to the implementation; udpstp still conforms. There are 117. Those marked as also an OB-UDPST Difference are readings OB-UDPST doesn’t share.

Load Rate Adjustment Algorithms (18)

shallow-queue-search-alternates-at-the-bottleneck

Through a queue too shallow for delay variation to reach upperThresh, Algorithm B confirms Congestion at its first overshoot below the 1 Gbps row: Fast Mode’s jump of highSpeedDelta rows past the bottleneck’s rate brings slowAdjThresh Errored Intervals back to back, and the third takes 3 × highSpeedDelta rows off, once. From then on, in Single-Step Mode, each Errored Interval takes one row off and each Clean one adds one back, and loss, the only signal such a queue gives, comes back a Trial Interval or two late, so the search settles into a cycle a few rows around the bottleneck’s rate and every Sub-Interval loses Load PDUs at the rows above it (5-8 % through 20 ms at 50 Mbit/s with the default seqErrThresh, about 2 % with 0). Above the 1 Gbps row there is no Fast Mode: the search climbs a row at a time, its first overshoot is one row, and it cycles the same way, but its Errored Intervals can come too few at a time to confirm Congestion, which it then never does: through 1 ms of queue at 2.5 Gbit/s, one or two at a time in the simulator, and in network namespaces through tbf two at a time, three in some Tests; through 20 ms, three at a time, which confirms it. The share of the Load PDUs the cycle loses in every Sub-Interval at the bottleneck’s rate grows with the row’s step against the rate: in the simulator about 2 % at 2.5 Gbit/s through 20 ms over a 10 ms round trip and 3-4 % at 1.2 Gbit/s, growing with the path’s delay at the points tested there. Above the 1 Gbps row, where the rows are 100 Mbps apart, it is often above 1 %; below it, from a few hundred Mbit/s up, near 1 %, which the path’s delay moves either way. The Client’s warning that no search found the path’s limit takes a search that never confirmed Congestion as having met it at its end: of its last 64 Trial Intervals, from the first Errored Interval among them on, at least 4 and at least 1 in 4 were errored. udpstp runs the algorithm as specified rather than adding a back-off of its own, and takes RFC 9097 §6.3’s maximum over every Sub-Interval that meets the Performance Criterion, the search’s climb included; §6.5 doesn’t say what a maximum from a Sub-Interval without congestion means in a Test with it. So where no Sub-Interval at the bottleneck’s rate meets the criterion, such a Test reports no valid maximum (ErrorStatus 49), or, where a Sub-Interval of the climb met it, at the latest the one in which the search first overshot, a Maximum IP-Layer Capacity from the climb, well below the bottleneck and with no warning: a cliff, not a bias. 1960 Mbit/s of 2.5 Gbit/s through tbf’s 20 ms in network namespaces; in the simulator 1885 Mbit/s there, Fast Mode’s last Sub-Interval, 893 Mbit/s, at 1 to 2 Gbit/s, and 287 Mbit/s at 500 Mbit/s over a 40 ms round trip, where the cycle loses 1.04 %. Either way it gives the Highest IP-Layer Capacity with its Loss Ratio; --max-loss-ratio accepts more loss.

Also an OB-UDPST Difference.
RFC: RFC 9097 §6.3, §6.5, §8.1
Requirements: RFC9097-6.3-L1, RFC9097-6.3-3, RFC9097-6.3-4, RFC9097-8.1-L11
OB-UDPST: OB-UDPST’s search takes the same steps (one row each way above the 1 Gbps row, the confirming drop only below it, Sequence Errors counted without reordering and duplicates), so while the queue’s delay stays under lowThresh it cycles the same way with the same loss: measured at 50 Mbit/s, and above the 1 Gbps row read from its code. Between lowThresh and upperThresh its Hold Intervals differ, since it judges the RTT variation by default (record delay-var-is-the-maximum-by-default). It reports the Highest IP-Layer Capacity as its maximum whatever the loss (record json-at-max-is-the-maximum), there the bottleneck’s rate.

Proving tests:

fast-mode-stops-at-the-gigabit-row

Fast Mode never passes the 1 Gbps row (row 1000). Algorithm B adds highSpeedDelta rows but stops at that row; algorithm C jumps to it when doubling would pass it. Above it, the search moves in Single-Step Mode, one row at a time, as a Test already there would. RFC 9097 §8.1’s prose says the rate is only increased by one above a high rate such as 1 Gbps; Y.1540 Amd 2 and TR-471’s flowcharts, and RFC 9097’s example pseudocode, add highSpeedDelta with no cap, so a search just below the 1 Gbps row could overshoot it by up to highSpeedDelta - 1 rows, which above row 1000 are OB-UDPST’s much larger jumbo rows. OB-UDPST caps it the same way, so results match.

RFC: RFC 9097 §8.1; RFC 9946 §5.1
Requirements: RFC9946-5.1-7, RFC9097-8.1-L12
References: docs/spec.md

Proving tests:

latched-congestion-confirmed

Congestion is confirmed by slowAdjThresh Errored Intervals back to back: any Clean or Hold Interval resets the count, following RFC 9946 §5.1(5)’s “in all of the consecutive intervals”. Congestion Confirmed is then latched, so algorithm B never leaves Single-Step Mode (RFC 9946 §7), and algorithm C clears it only in a Fast-Mode Retry. Resetting the flowchart’s SlowAdjCount instead would let algorithm B return to Fast Mode and oscillate.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-L1
OB-UDPST: Keeps the flowchart’s single SlowAdjCount, reset only by a Fast Mode pass, so errored, hold, errored, hold, errored also confirms Congestion. OB-UDPST can confirm Congestion earlier on paths with intermittent errors, so the two can report different capacities on the same path.
References: ADR 0005

Proving tests:

confirming-drop-below-the-gigabit-row-only

The Errored Interval that confirms Congestion takes 3 × highSpeedDelta rows off, stopping at row 0, only when the row is below the 1 Gbps row, as the flowchart’s rate < HSpeedThresh and RFC 9097’s pseudocode compare it. At or above the 1 Gbps row it takes one row off, like any other Errored Interval. The comparison is on row indexes: row N is N Mbps up to row 1000.

RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-7

Proving tests:

algorithm-c-doubles-the-row-index

Algorithm C’s “Increase Sending by 2x” doubles the Sending Rate Table row index, on every second Clean Interval in Fast Mode. Row 0 goes to row 1 on the first Fast Mode pass. Row N is N Mbps up to row 1000, so doubling the row doubles the rate there.

RFC: RFC 9946 §5.1

Proving tests:

retry-thresh-grows-by-5-after-a-step-up

RetryThresh starts at 5 and grows by 5 after a Fast-Mode Retry reached from a single step up (5, 10, 15, …), as the flowchart footer’s RETRY_THRESH_ALGOC and OB-UDPST have it; a retry reached from a single step down leaves it as it is. TR-471’s table text says it doubles (5, 10, 20, 40).

RFC: RFC 9946 §5.1
References: docs/spec.md

Proving tests:

retry-count-counts-actual-single-steps

Only a single step that moves the row counts toward RetryThresh: a Clean Interval at the Test’s highest row, or an Errored Interval at row 0, is none, and neither is the confirming drop. A Lost Status Backoff that moves the row one down counts. Y.1540 Amd 2 Table B.3 counts every status report; OB-UDPST counts as we do.

RFC: RFC 9946 §5.1

Proving tests:

fast-mode-retry-resets-the-errored-run

A Fast-Mode Retry clears Congestion Confirmed and also resets the count of back-to-back Errored Intervals, as the flowchart resets SlowAdjCount. This is a reset beyond ADR 0005’s reset by any Clean or Hold Interval: a retry reached on an Errored Interval starts the next run from 0.

RFC: RFC 9946 §5.1
References: ADR 0005

Proving tests:

update-persists-across-a-fast-mode-retry

Algorithm C’s Update flag toggles on each Fast Mode pass that doesn’t jump to the 1 Gbps row, and keeps its value across a Fast-Mode Retry, so the first pass after a retry may double the row or hold it. The documents don’t say whether a retry resets it; OB-UDPST keeps it too.

RFC: RFC 9946 §5.1

Proving tests:

lost-status-backoff-is-an-errored-interval

A Lost Status Backoff counts as an Errored Interval, following RFC 9097 §8.1’s “same process” with the algorithm’s variables “in their current state”: it adds to the back-to-back run, can confirm Congestion with the one-time drop of 3 × highSpeedDelta rows, and otherwise takes one row off. RFC 9097 describes it for algorithm B only; under algorithm C a backoff that moves the row also counts toward RetryThresh, so a long feedback outage can cycle through confirmation, drop and Fast-Mode Retry. At the defaults the third backoff, about 290 ms without a Status PDU, confirms Congestion.

Also an OB-UDPST Difference.
RFC: RFC 9097 §8.1
Requirements: RFC9097-8.1-13, RFC9097-8.1-L21
OB-UDPST: Has no Lost Status Backoff: it keeps its rate until its no-traffic timeouts.
References: ADR 0005

Proving tests:

lost-status-backoff-initial-w

RFC 9097 §8.1 lets a path whose RTT is longer than trialInt start w above 0. The Server starts w at the Setup Response’s round trip (from sending it to the Activation Request’s arrival) divided by trialInt, rounded up, capped so the first Lost Status Backoff still comes before the 1 s Status PDU timeout. Without it a path with an RTT above about 140 ms backs off three times before its first Status PDU can arrive and confirms Congestion at the start. Once a Status PDU arrives, w starts over at 0.

RFC: RFC 9097 §8.1
Requirements: RFC9097-8.1-14, RFC9097-8.1-L20

Proving tests:

stale-status-pdus-count-only-as-heard

A Status PDU whose spduSeqNo isn’t greater than the last one processed, or is 0, is stale, in both roles: it isn’t fed to the Load Rate Adjustment Algorithm, can’t change the Sending Rate Structure or deliver STOP, and isn’t echoed for RTT. It still counts as heard for the Watchdog and Lost Status Backoff, its Sub-Interval statistics still count, and it is counted (DataEvent::StaleStatusPdus). RFC 9097 runs the algorithm on each “new status report” without saying whether a reordered or duplicated one is new; this rule keeps a replayed Status PDU from steering or stopping a Test.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-4, RFC9946-8.2-L2
OB-UDPST: Feeds every Status PDU, reordered or duplicated ones included, to the algorithm in a Downstream Test.
References: docs/spec.md story 92

Proving tests:

spdu-seq-err-is-the-gap-before-the-latest

The Load Sender’s spduSeqErr is the number of Status PDUs missing just before the latest one processed, not a running total, as OB-UDPST counts it; a gap above 0xFFFE is sent as 0xFFFE. RFC 9946 §8.1 calls it “the Status PDU loss count” without saying over what span.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-L8
OB-UDPST: Sends 0xFFFF for a reordered Status PDU; udpstp treats that one as stale and never sends 0xFFFF.

Proving tests:

clean-needs-delay-var-below-low-thresh

A Clean Interval needs DelayVar strictly below lowThresh, as RFC 9097’s pseudocode and OB-UDPST have it; the Y.1540 and TR-471 flowcharts say <=. A DelayVar equal to lowThresh makes a Hold Interval.

RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-9

Proving tests:

seq-err-thresh-always-applied

RFC 9946 §5.1(6) says Congestion MUST be indicated when Sequence Errors are detected, then recommends a threshold of 10. udpstp always applies seqErrThresh: a Trial Interval is errored only when its Sequence Errors exceed it, and 0 is honoured, so any Sequence Error is errored then. The flowcharts, RFC 9097’s pseudocode and OB-UDPST do the same.

RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-9

Proving tests:

no-delay-sample-is-never-clean

A Trial Interval without a delay sample is never clean: it is a Hold Interval unless its Sequence Errors exceed seqErrThresh. A Trial Interval in which no Load PDU arrived is one too, since its loss shows only once a later Load PDU arrives. The documents don’t cover the case; OB-UDPST also treats it as no change.

RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-9

Proving tests:

stop-status-pdus-are-not-classified

A Status PDU whose testAction is STOP is not classified and doesn’t move the rate: it ends the Test rather than reporting a Trial Interval to adjust on. This holds for the Status PDUs the Server receives in a Downstream Test and those it sends in an Upstream Test. Once the Server has sent STOP1 it classifies none, even those still marked TEST: the Test is ending, so the rate no longer matters. Everything else about the PDUs received then is processed normally (RFC 9946 §9): they count in the results, reset the Watchdog, and give sequence numbers and RTT samples.

RFC: RFC 9946 §8.2, §9
Requirements: RFC9946-9-L2

Proving tests:

ipv4-based-sending-rate-structure-sizes

The payload sizes in the Sending Rate Structure are IPv4-based (IP packet size minus 28 bytes), and a Load Sender on IPv6 sends each datagram 20 bytes smaller, unless that would leave less than a Load PDU header, so packet sizes and rates are the same on both address families. This reconciles RFC 9946 §8.1’s “use the parameters unchanged” with §6.1’s maximum IP packet sizes, which a literal reading breaks on IPv6. The IP version comes from each Test Connection’s packets, never the socket family: an IPv4-mapped peer is IPv4. OB-UDPST uses the same convention, and our Client can’t tell an OB-UDPST Server from another, so both roles always apply it.

RFC: RFC 9946 §6.1, §8.1
Requirements: RFC9946-6.1-8, RFC9946-6.1-9, RFC9946-6.1-11, RFC9946-6.1-12, RFC9946-7.1-5, RFC9946-8.1-1, RFC9946-8.1-6
References: ADR 0004

Proving tests:

The Client (20)

mc-ident-from-a-held-port

The Client draws a Test’s mcIdent from a UDP socket it holds until the Test ends, connected toward the first Server: the socket’s port, XORed with a 16-bit hash of its local address, a mapping one-to-one over the non-zero ports. No other socket on the host holds that port meanwhile, so Clients on one host never share an mcIdent at once; a Server keys a Test on the Client’s address and mcIdent, and two Clients drawing the same one at random (about a 26 % chance among 200) cost a Test: the Server merged them and refused one (MCINVPAR, MultiConnectionIndexInUse), or dropped an identical Setup Request, sent in the same second, as a replay. The replay cache is unchanged. Hosts behind one NAT have different local addresses, so their mcIdents are spread as a random draw’s; hosts sharing one private address behind it (VMs on a hypervisor’s NAT, cloned images) share the hash, and draw only as widely as the host’s local port range, about 28,000 values on Linux. If the host gives no such socket, the draw is random.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1
Requirements: RFC9946-6.1-L3
OB-UDPST: OB-UDPST’s Client draws mcIdent at random, and its Server doesn’t check that an mcIndex is unused within a Test.
References: docs/spec.md story 93

Proving tests:

setup-response-from-another-address

The Client authorizes destinations from local Server selection before receiving PDUs. In authenticated modes a Setup Response must come from the selected Server address or an alternate IP address explicitly authorized for that Server with –allow-server-address SERVER=IP, using its Standard Port, address family and locally selected IPv6 scope. Its verified digest authorizes the Ephemeral Port, never a new IP address or interface. Other selected Servers, other DNS answers and Null Requests do not add authorization. Unauthenticated Mode still accepts only the selected Server endpoint. The Activation exchange and Data Phase use only the authorized endpoint.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.3, §7.1
Requirements: RFC9946-6.3-1, RFC9946-7.1-1
OB-UDPST: OB-UDPST’s Client connects to whatever address the Setup Response came from, unauthenticated too.
References: ADR 0007, ADR 0009, ADR 0013

Proving tests:

unverified-setup-rejection-hint

A Setup Response that fails verification is ignored and the Test initiation timer decides the outcome, so a forged datagram can’t end a genuine Test (ADR 0009). When such a response comes from the Server’s address and rejects this Test Connection’s own request, its cmdResponse is still reported, marked unverified, as a hint to why the timer expired; an unsigned AUTHINV to a mode 2 request adds the diagnostic that the Server probably doesn’t support mode 2.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.3.1, §6.3
Requirements: RFC9946-6.3-1
OB-UDPST: OB-UDPST’s Client ends the Test at once on a Setup Response that fails verification, with ErrorStatus 107 or 108.
References: ADR 0009

Proving tests:

undefined-setup-cmd-response

A verified Setup Response whose cmdResponse is neither ACKOK nor a code §12.3.5 defines as an error, CHSR_CRSP_NONE (0) included, ends the Setup attempt as an unknown response.

RFC: RFC 9946 §6.3, §12.3.5
Requirements: RFC9946-6.3-3, RFC9946-6.3-4

Proving tests:

setup-response-protocol-version

The Client ignores a Setup Response whose protocolVer isn’t 20, as it ignores one with another PDU ID or cmdRequest, except a BADVER rejection, which carries the version the Server expects (§6.2.1) and is reported as such. §6.3 says to begin with the protocol version but not what to do when it differs.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.3
Requirements: RFC9946-6.3-2
OB-UDPST: OB-UDPST’s Client checks neither the Setup Response’s protocolVer nor its cmdRequest.

Proving tests:

minimum-test-connections

A Multi-Connection Test may go on with fewer Test Connections than it asked for, down to the Minimum Test Connections the Client user sets. By default all are required, so one failed Setup aborts the Test before any Activation Request is sent. RFC 9946 §4 leaves handling connection setup successes and failures to the Client process.

RFC: RFC 9946 §4
Requirements: RFC9946-4-L1
By default: off
Setting: --min-test-connections
References: docs/spec.md stories 56 and 57

Proving tests:

max-bandwidth-split-across-test-connections

The maxBandwidth the Client user gives (--max-bandwidth) is the whole Test’s: the Client splits it equally across the Test’s Test Connections, each Setup Request stating its share rounded down to a whole Mbps, so together they state no more than was asked for, and a figure below 1 Mbps a Test Connection is refused. RFC 9946 §6.1 defines maxBandwidth as what the Client expects “during the requested test”, which in a Multi-Connection Test could mean the Test or one Test Connection; a Server compares each Setup Request’s maxBandwidth with what it has left to admit, and searches that Test Connection no higher.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1
Requirements: RFC9946-6.1-L7, RFC9946-6.1-6
OB-UDPST: OB-UDPST’s Client splits it the same way, but states at least 1 Mbps a Test Connection rather than refusing a smaller figure, so its Test Connections may together state more than was asked for.
References: docs/spec.md story 9

Proving tests:

multi-connection-setup-deadline

Every Setup Request goes out at once and the Activation Requests go out together. The Setup phase waits for every Setup until a deadline 1 s before the Client’s time limit (or half the limit, if shorter), so the Activation exchange has time left within the Test initiation timer. Past it, Setups still unanswered are abandoned once at least the Minimum Test Connections have succeeded; with fewer, the Setup phase waits on to the time limit.

RFC: RFC 9946 §6.1
Requirements: RFC9946-6.1-2
References: docs/spec.md story 57

Proving tests:

multi-connection-parameters-differ

Each Test Connection’s parameters, as its Activation Response sets them, are compared with the first activated Test Connection’s. If they differ, the parameters aren’t acceptable for one Test, and the Client aborts it (ErrorStatus 203).

RFC: RFC 9946 §7.3
Requirements: RFC9946-7.3-L1
References: docs/spec.md story 58

Proving tests:

multi-connection-isolated-failures

A Test Connection fails when its Data Phase stops hearing its peer (the 1 s Load PDU or Status PDU timeout) or ends any way but complete. The Test goes on while at least the Minimum Test Connections haven’t failed; below it, the healthy ones are stopped. RFC 9946 §4 makes cleaning up failed connections during a Test the Client process’s responsibility.

RFC: RFC 9946 §4
Requirements: RFC9946-4-L1
References: ADR 0002, docs/spec.md story 59

Proving tests:

multi-connection-path-failure

Two or more Test Connections falling silent within three Trial Intervals of each other are a path failure, and stop the Test even when at least the Minimum Test Connections remain. The Load Rate Adjustment Algorithm ends a Test on a loss of connectivity (§4.2), and a Test’s connections to one Server share a path. Three Trial Intervals allow for a Load Sender hearing its peer once a Trial Interval, and for a lost last Status PDU.

RFC: RFC 9946 §4, §4.2
Requirements: RFC9946-4.2-L5
References: docs/spec.md story 60

Proving tests:

sub-interval-results-from-the-test-only

Only Sub-Intervals that closed while testAction was TEST are results, up to the number the Test Interval holds (testIntTime / subIntPeriod). On the Load Sender the first sisSav received for a subIntSeqNo counts, from a stale Status PDU too; later copies replace nothing. A Sub-Interval never measured, such as a final one whose Status PDU was lost, is missing, never a zero.

RFC: RFC 9097 §5.3, RFC 9946 §8.2
References: docs/spec.md

Proving tests:

duplicates-count-as-received

For the Performance Criterion and the Loss Ratio reported, the Load PDUs sent in a Sub-Interval are those received plus those lost, and duplicates count as received: “correctly received” isn’t read as excluding them. Duplicates are still reported apart from loss.

RFC: RFC 9097 §5.3, §6.3
Requirements: RFC9097-5.3-L1, RFC9097-6.3-L1

No proving test.

loss-allowance-per-test-connection

A Sub-Interval meets the Performance Criterion if it lost at most the configured ratio of the Load PDUs sent, or a single Load PDU, so one loss at a low rate doesn’t disqualify it. In a Multi-Connection Test’s aggregate the allowance is one Load PDU per active Test Connection, so the aggregate is judged no more harshly than its Test Connections alone.

RFC: RFC 9097 §6.3
Requirements: RFC9097-6.3-3

Proving tests:

empty-sub-interval-meets-no-criterion

A Sub-Interval in which no Load PDU was received meets no Performance Criterion, so it is never the Maximum IP-Layer Capacity, even when the one-lost-Load-PDU allowance covers what was lost in it: the maximum is taken over bits transmitted and correctly received that meet the PM criteria, and none were received. With nothing lost either, its Loss Ratio is 0/0, undefined rather than zero. Otherwise a gap covering a whole Sub-Interval, such as a stall just under the 1 s traffic timeout, or an Upstream Load Receiver sending Status PDUs through a silence, made an empty Sub-Interval the only one to pass when every other failed the criterion, and the Test reported 0 Mbps as its maximum where it has none (ErrorStatus 49).

RFC: RFC 9097 §6.3
Requirements: RFC9097-6.3-L1

Proving tests:

tmax-within-the-sub-interval

Tmax, the longest a Load PDU may take and still count as received, is the rest of the Sub-Interval whose lpduSeqNo gap counted it as lost, so at most one Sub-Interval: a late Load PDU cancels that loss only within that Sub-Interval. TR-471 suggests 1000 ms; tying Tmax to the Sub-Interval keeps each Sub-Interval’s loss final when it closes.

RFC: RFC 9097 §4, §5.3
Requirements: RFC9097-4-L4, RFC9097-5.3-4, RFC9097-6.3-5

Proving tests:

aggregate-clock-delta-min

A Multi-Connection Test’s aggregate Sub-Interval takes the smallest clockDeltaMin of its Test Connections, as it does rttMinimum. Several Servers’ clocks differ, so clockDeltaMin is kept only when every Test Connection goes to one Server, and is left out otherwise. Each Test Connection fixes its wall-clock offset when it activates, so a wall-clock step between activations shifts the aggregate.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4, §8.2
Requirements: RFC9946-4-L1
OB-UDPST: OB-UDPST takes the smallest clockDeltaMin of all Test Connections, whatever Servers they go to.
References: docs/spec.md

Proving tests:

multi-connection-common-grid

In a Downstream Multi-Connection Test the Client’s Load Receivers share one Sub-Interval grid, starting when the first Load PDUs should arrive: when the Activation Requests go out plus the shortest Setup round trip. The Test Connections’ Sub-Intervals of one subIntSeqNo then cover the same time, and their sum is one Sub-Interval’s IP-Layer Capacity. The last Sub-Interval ends before the Servers’ STOP, half a second after the Test Interval. A single Test Connection keeps its own grid.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.2, RFC 9097 §5.3
Requirements: RFC9097-5.3-L1
OB-UDPST: OB-UDPST sums the Test Connections’ Sub-Intervals by number, each on its own grid, in both directions.
References: docs/spec.md

Proving tests:

usdir-set-on-every-upstream-setup

The Client sets CHSR_USDIR_BIT in every Upstream Setup Request, whether or not it states a maxBandwidth, since the bit says which direction the maxBandwidth field is for.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1
Requirements: RFC9946-6.1-L8
OB-UDPST: An OB-UDPST Client sets the bit only when it states a maxBandwidth (-B); otherwise the whole field is 0.

Proving tests:

results-name-the-phase-and-f

A Test is one Phase of RFC 9097 §9’s tables: a Search Test or a Fixed-Rate Test, with F Test Connections. The text names Phase and F once in its Results Summary heading and parameters (“Downstream Search Test”, “1 Test Connection (F)”), for both the Maximum IP-Layer Capacity per Capacity Mode and the table of IP-Layer Sender Bit Rates. The whole Test’s maxima aggregate all its Test Connections; individual maxima appear under each Test Connection’s heading. The JSON names both in TestType and NumberOfConnections.

RFC: RFC 9097 §9
Requirements: RFC9097-9-3, RFC9097-9-4, RFC9097-9-10

Proving tests:

The Control Phase and Admission (39)

auth-time-window-inclusive-modulo-2-32

A received authUnixTime is acceptable within 5 s of the receiver’s wall clock, in whole seconds, both edges included. The difference is taken modulo 2^32 as a signed 32-bit value, so times either side of the field’s 2106 wraparound compare correctly.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.3.1
Requirements: RFC9946-5.3.1-4
OB-UDPST: OB-UDPST compares 64-bit times and would reject every PDU after the 2106 wraparound.
References: ADR 0009

Proving tests:

authentic-setup-outside-time-window-is-silent

A Setup Request whose digest verifies but whose authUnixTime is outside the window is dropped silently. The §6.2 note makes it invalid, which comes before §6.2.1’s rule that a valid authDigest gets a coded rejection. Only Troubleshooting Mode answers it, with AUTHTIME.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.2, §6.2.1
Requirements: RFC9946-6.2-2, RFC9946-6.2.1-3
OB-UDPST: OB-UDPST always answers with CHSR_CRSP_AUTHTIME.
References: ADR 0009

Proving tests:

activation-auth-matches-setup

An Activation Request must carry its Setup Request’s authMode and keyId, checked before the digest as part of “authMode … appropriate” (§6.2 step 3); a mismatch is dropped silently like any other verification failure.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.4.1, §6.2, §7.2.1
Requirements: RFC9946-7.2-2, RFC9946-7.2.1-6, RFC9946-5.4.1-8
OB-UDPST: OB-UDPST doesn’t check the Activation Request’s keyId; its Client always sends the Setup’s.
References: ADR 0009

Proving tests:

troubleshooting-replies-unsigned-except-authtime

In Troubleshooting Mode a Setup Request that fails authentication gets a Setup Response carrying the reason. It is unsigned, since a failed digest leaves no valid key, except AUTHTIME: its digest verified, so it is signed with the Server Authentication Key. An unknown keyId, or a key outside its lifetime, gets AUTHFAIL like a bad digest.

RFC: RFC 9946 §6.2.1
Requirements: RFC9946-6.2.1-L1
By default: off
Setting: --troubleshooting, troubleshooting in TOML
References: ADR 0009

Proving tests:

troubleshooting-replies-rate-limited

Troubleshooting Mode answers at most 10 authentication failures a second, since a spoofed request gets its reply, and the Server warns every minute while it is on. Replays stay silent. Activation Requests that fail verification stay silent even then, although §7.2.1 permits an answer.

RFC: RFC 9946 §6.2.1, §7.2.1
Requirements: RFC9946-6.2.1-L1, RFC9946-7.2.1-L4
By default: off
Setting: --troubleshooting, troubleshooting in TOML

Proving tests:

server-limits-refuse-with-connfail

The Server’s own limits are refused with CHSR_CRSP_CONNFAIL: Tests per source, Test Connections per Test (24 by default) and in all, a Source’s budget, and a socket that can’t be opened. So an mcCount above 24 isn’t refused with MCINVPAR; the 25th Test Connection gets CONNFAIL. The Admitted Rate Cap and one Test’s share of it give CAPEXC. Every Activation refusal is CHTA_CRSP_BADPARAM, the only code §12.3.9 defines.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.2.1, §7.2.1, §12.3.5
Requirements: RFC9946-6.2.1-3, RFC9946-7.2.1-L2, RFC9946-12.3.5-L1
OB-UDPST: OB-UDPST refuses an mcCount above 24 with MCINVPAR, and sends CONNFAIL only when its connection table is full or allocation fails.

Proving tests:

per-test-cap-over-the-tests-life

The Test Connections per Test cap counts over the Test’s whole life, closed ones included, so a Test kept alive by one Test Connection can’t start new measurements under it. A Test ends when none of its Test Connections is admitted any more, one waiting for its socket included; a Setup from the same address and mcIdent after that starts a new Test.

RFC: RFC 9946 §11
Requirements: RFC9946-11-4

Proving tests:

multi-connection-agreement-checks

The Test Connections of a Test (one Client address and mcIdent) must agree: mcIdent non-zero, mcIndex below mcCount and unique within the Test, the same mcCount and authMode, and the same keyId in an authenticated mode; otherwise MCINVPAR. This is stricter than §4, which lets a Server treat each Test Connection as if from a separate Client; §6.1 lets a Server use mcIdent to associate them. The Test’s other mcIndexes are never waited for.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4, §6.1
Requirements: RFC9946-6.1-L1, RFC9946-6.1-L3, RFC9946-4-L2
OB-UDPST: OB-UDPST checks only mcCount at most 24 and mcIndex below mcCount, keeps no per-mcIdent state, and accepts mcIdent 0.

Proving tests:

duplicate-setup-request-dropped

A copy of an admitted Setup Request (same address, mcIdent and mcIndex) is dropped silently, not refused as an mcIndex in use, so a datagram the network duplicated can’t end the Client’s Test.

RFC: RFC 9946 §6.2.1

Proving tests:

replay-cache-for-setup-requests

The replay cache remembers a verified Setup Request’s keyId and authDigest, only once it has passed the digest and time checks and is within its Source’s budget, so a flood can’t fill it faster than the budgets allow; a copy of one refused by its budget is judged afresh once the budget refills. A copy is dropped silently. Two Clients sharing a key that send byte-identical requests (same mcIdent, parameters and second) collide: the second is dropped as a replay.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.3.1
Requirements: RFC9946-5.3.1-4
OB-UDPST: OB-UDPST has no replay cache.

Proving tests:

replay-cache-activation-keyed-on-sender

Activation Requests are remembered by keyId, authDigest and sender address: the Test Connections of a Multi-Connection Test set up in the same second send byte-identical Activation Requests. One cache serves every socket, and an Activation Request must come from its Setup’s Client IP address, so a verbatim copy sent to a sibling’s Ephemeral Port is caught; a copy spoofed from another port of that address isn’t.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.3.1
Requirements: RFC9946-5.3.1-4
OB-UDPST: OB-UDPST has no replay cache.

Proving tests:

replay-cache-lifetime-and-capacity

An entry lives 11 s on the monotonic clock: 5 s after an authUnixTime stamped up to 5 s ahead, plus the truncated second. A wall-clock step back of 5 s or more can let an expired request in again. The cache is sized from the global budget (every Setup Request it lets through in 11 s, plus four Activation Requests per Test Connection; 220k entries by default, at most about 2 million) and refuses new verified requests while full.

RFC: RFC 9946 §5.3.1
Requirements: RFC9946-5.3.1-4

Proving tests:

unverified-setup-budget

A global budget, 20000 a second by default, caps the Setup Requests that reach a KDF run and an HMAC; the rest are dropped silently. It is spent once a Key Table row is found, so unknown keyIds and keys outside their lifetimes are dropped first without spending it, and by every allowed request in Unauthenticated Mode. Activation Requests draw on no budget. It is a fixed one-second window, which admits Setup Requests that arrive together, such as a Multi-Connection Test’s, all together while it has room.

RFC: RFC 9946 §11
By default: on
Setting: --unverified-setups-per-second, unverified_setups_per_second in TOML

Proving tests:

source-budget-spent-after-the-hmac

Each Source (an IPv4 address or IPv6 /64) has a budget of authentic Setup Requests, 48 at once and 5 more a second by default, spent only after the digest verifies, so a spoofed source can’t spend another’s. A key holder still can, by signing requests from its address. Requests in Unauthenticated Mode have no HMAC and spend none. Beyond the budget, only the first refusal is answered (conformance: authentic-setup-requests-left-unanswered).

RFC: RFC 9946 §11
By default: on
Setting: --source-setup-burst, --source-setups-per-second

Proving tests:

key-setup-budget

Each Key Table row has a budget of Setup Requests, 1000 at once and 1000 more a second by default, Unauthenticated Mode counting as one row. Operator Keys spend it too. It is spent after the digest verifies and the replay check, so a spoofed request spends none, and a replayed one at most once (a request the budget refused isn’t remembered, so a captured copy can come back once), and a request over it is dropped silently, never answered, unlike the per-Source budget’s first refusal, so a key holder’s flood gets no reflection. A key holder’s flood of authentic Setup Requests then costs the Server at most its key’s budget in admissions a second, and the rest what a forged request within the unverified budget costs (a KDF run and an HMAC, no admission), so it can’t saturate the control thread and, through the Server’s shared queue, keep other keys’ Clients from starting Tests. The flooding key’s own Clients share its budget: a trust-model limit (docs/security.md).

Also an OB-UDPST Difference.
RFC: RFC 9946 §11
OB-UDPST: OB-UDPST has no per-key budget.
By default: on
Setting: --key-setup-burst, --key-setups-per-second

Proving tests:

path-approximated-by-source

The path of §4.2’s one concurrent Test per path is approximated by the source: an IPv4 address, or an IPv6 /64 (IPv4-mapped addresses count as IPv4), so link-local and Teredo Clients may share one. A Test is the Client’s IP address plus mcIdent. Only activated Tests count, so a spoofed Setup Request can’t keep a source from testing; the first Test Connection to activate checks again, and a Test that no longer fits is refused with BADPARAM.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4.2
Requirements: RFC9946-4.2-1
OB-UDPST: OB-UDPST has no per-source or per-path limit.

Proving tests:

admitted-rate-cap-per-direction

The Admitted Rate Cap applies to each direction separately: Upstream and Downstream Test Connections each sum their stated maxBandwidth (CHSR_USDIR_BIT masked) against it, as a link carries its rate both ways. docs/spec.md says a cap on total admitted maxBandwidth. A Setup Request stating no maxBandwidth is refused with NOMAXBW only when the Server requires one.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1
Requirements: RFC9946-6.1-L7, RFC9946-6.1-L8
OB-UDPST: OB-UDPST also keeps a pool per direction, but with -B set it always refuses a maxBandwidth of 0 with NOMAXBW.
By default: off
Setting: --admitted-rate-cap, --require-max-bandwidth

Proving tests:

usdir-must-match-activation-direction

With an Admitted Rate Cap set, an Activation Request whose direction differs from its Setup Request’s CHSR_USDIR_BIT is refused with BADPARAM, since bandwidth admitted for one direction can’t be used in the other. Only when the Setup stated a nonzero maxBandwidth: OB-UDPST’s Client sets the bit only then. Without a cap the bit decides nothing.

RFC: RFC 9946 §6.1, §7.2.1
Requirements: RFC9946-6.1-L8

Proving tests:

admitted-rate-reserved-at-activation

Only activated Tests hold the Admitted Rate Cap: a Test reserves its whole stated rate, in each direction its Test Connections state, when its first Test Connection activates, so a Setup never activated can’t keep other Tests out of the cap. From then on each of its admitted Test Connections holds its rate, one still waiting for its socket included. Of Tests admitted together past the cap, one that no longer fits when it activates is refused whole with BADPARAM, never left half running.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1, §11
Requirements: RFC9946-11-4
OB-UDPST: OB-UDPST reserves the rate at Setup.

Proving tests:

max-test-share-of-the-rate-cap

A Test without an Operator Key may state at most a share of the Admitted Rate Cap, all its Test Connections together: half by default, so at least two Tests can run whatever one states. An activated Test holds its whole maxBandwidth for its whole Test Interval, whatever it sends. A Setup past the share gets CAPEXC.

Also an OB-UDPST Difference.
RFC: RFC 9946 §11
Requirements: RFC9946-11-4
OB-UDPST: OB-UDPST lets one Test take the whole cap.
By default: on
Setting: --max-test-share, max_test_share in TOML

Proving tests:

unactivated-test-connection-share

Unactivated Test Connections have a separate share outside the global Test Connection cap: half that cap by default, at least the per-Test cap, and configurable above the global cap. The Ephemeral Port range must hold their sum. Activated Tests alone reserve global places, including admitted siblings awaiting their socket or activation. The first activation reserves the whole Test; if it no longer fits, BADPARAM (TestConnections in the log) refuses it and its remaining siblings until all close. The global cap is checked at Setup too, against reserved places, so a full cap still refuses without evicting an unrelated unactivated Test. When the unactivated share is full, a Setup that would otherwise be admitted closes one Unactivated Test Connection (Event::UnactivatedTestConnectionEvicted), as a SYN cache evicts, rather than being refused, so Setup Requests never activated can’t fill the global cap. It closes the oldest, by its Setup Response, of the Key Table row holding the most Unactivated Test Connections, those of Unauthenticated Mode counting as one group; of groups holding as many, the one whose oldest is oldest, so with one group the oldest of all. Setups still waiting for their socket count toward the share and their group but aren’t closed, so a group holding the most only in those closes no one’s and the new Setup is refused, keeping the share bounded. A key flooding authentic Setup Requests that never activate therefore evicts its own, and another key’s Client, a Multi-Connection Test’s Test Connections included, keeps its place until it activates, at any round trip within the Activation Watchdog, while that key holds fewer than half the share; the oldest of all would let such a flood admitted at R a second close any Client’s about 128/R seconds after it was admitted (at the default share). It is a fair share, not a guarantee: a flood split over k groups holds the share in about k + 1 equal parts, so another key holding more than about share/(k + 1) at once loses some. The flooding key’s own Clients, and in Unauthenticated Mode every unauthenticated Client, are not protected: a trust-model limit (docs/security.md). RFC 9946 says which Test Connection to close no more than it says to close one.

Also an OB-UDPST Difference.
RFC: RFC 9946 §11
Requirements: RFC9946-11-4
OB-UDPST: OB-UDPST has no such share; unactivated Test Connections hold their slot until the 3 s watchdog.
By default: on
Setting: --max-unactivated-test-connections, max_unactivated_test_connections in TOML

Proving tests:

first-verified-activation-connects

An Activation Request must come from its Setup’s Client IP address; the port may differ, for NATs that change it. The first one that passes verification connects the Test Connection’s socket to its address and port, before any answer goes out; in Unauthenticated Mode, the first that decodes.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.2.2, §7.2
OB-UDPST: OB-UDPST accepts the Activation Request from any address and port and connects to whichever sent it.

Proving tests:

test-has-one-direction

A Test has one direction, fixed by its first accepted Activation Request; a Test Connection of the same Test asking for the other direction is refused with BADPARAM. RFC 9946 puts the direction on each Test Connection, and §7.2.1 lets the Server reject a request.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4, §7.1, §7.2.1
Requirements: RFC9946-7.2.1-L2
OB-UDPST: OB-UDPST keeps no per-Test state, so one mcIdent may mix directions (its own Client never does).

Proving tests:

fixed-rate-needs-operator-key

A Fixed-Rate Test (srIndexConf a row, CHTA_SRIDX_ISSTART clear) is refused with BADPARAM unless the Test Connection’s key is an Operator Key, since a consumer MUST NOT be able to initiate one. OB-UDPST key files have no role column, so an OB-UDPST Client needs a row our TOML Key Table marks as an Operator Key.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4.1
Requirements: RFC9946-4.1-3
OB-UDPST: OB-UDPST accepts a Fixed-Rate Test from anyone and never sends BADPARAM; an OB-UDPST Client given BADPARAM exits with 152.

Proving tests:

sr-index-def-ignores-isstart

srIndexConf CHTA_SRIDX_DEF asks for the Server’s default search from row 0 whatever CHTA_SRIDX_ISSTART says; it is never a Fixed-Rate Test.

RFC: RFC 9946 §7.1
Requirements: RFC9946-7.1-3, RFC9946-7.1-4

Proving tests:

sr-index-conf-beyond-the-top-row-coerced

An srIndexConf above the Server’s top row (the first row at the table’s top rate, 1069 with the default 1500-byte MTU) is coerced to it and reported, as §7.2.1’s configured maximum, for a Fixed-Rate row and, where the start-row cap allows it (an Operator Key, or max_start_row raised), a starting row.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.2.1
Requirements: RFC9946-7.2.1-3
OB-UDPST: OB-UDPST does not apply the same top-row coercion.

Proving tests:

search-capped-at-stated-max-bandwidth

The Server never attempts a rate above the Client’s stated maxBandwidth: the search tops out at the highest row within it, in both directions, with or without an Admitted Rate Cap. The CHSR_USDIR_BIT is masked and ignored for this cap. Row M offers M Mbps with IPv4 headers (ADR 0004), so over IPv6 the cap is exceeded by about 1.6%.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1, §11
Requirements: RFC9946-6.1-6, RFC9946-11-5, RFC9946-11-L2
OB-UDPST: OB-UDPST caps the search only in a RATE_LIMITING build with -B set, so an OB-UDPST Client stating maxBandwidth measures higher against an OB-UDPST Server than against ours.
References: ADR 0004

Proving tests:

row-above-max-bandwidth-coerced

A requested starting row or Fixed-Rate row above the stated maxBandwidth’s row is coerced to it and reported as an srIndexConf coercion, not rejected.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.2.1
Requirements: RFC9946-7.2.1-3, RFC9946-7.2.1-4
OB-UDPST: OB-UDPST doesn’t tie srIndexConf to maxBandwidth; it echoes a row up to its -I maximum.

Proving tests:

consumer-start-row-capped

A Test without an Operator Key may start its search no higher than row 100 by default; a higher starting row is coerced to it and reported. It may still climb past it. Below the 1 Gbps row the algorithms keep Fast Mode and their large step down, so a search started too high on a slow link drains in about 3 s; from row 1000 Algorithm B would step down one row a Trial Interval, flooding the link for the whole Test. Operator Keys are exempt.

Also an OB-UDPST Difference.
RFC: RFC 9946 §4.2, §7.2.1, §11
Requirements: RFC9946-7.2.1-2, RFC9946-7.2.1-3, RFC9946-11-5
OB-UDPST: OB-UDPST lets any Client start at any row up to its -I maximum, the whole table by default.
By default: on
Setting: --max-start-row, max_start_row in TOML

Proving tests:

use-ow-del-var-defaults-to-true

useOwDelVar defaults to True (one-way delay variation), as RFC 9946 §7.1 says, although the TR-471 EnableIPDV it cites defaults to False.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.1
Requirements: RFC9946-7.1-L4
OB-UDPST: OB-UDPST defaults to False (RTT variation). Interop runs pin it to False.

Proving tests:

parameter-ranges-follow-y1540

The Activation parameters’ ranges come from Y.1540 Amd 2 Tables B.1 and B.2 first: thresholds 5 to 250 ms, trialInt 5 to 250 ms (TR-471 advises 20 to 250), testIntTime 5 to 60 s, subIntPeriod 100 to 10000 ms, highSpeedDelta and slowAdjThresh from 2, seqErrThresh 0 to 100. The number of Sub-Intervals isn’t bounded (Y.1540 gives 1 to 100).

RFC: RFC 9946 §7.1, §7.2.1
Requirements: RFC9946-7.2.1-L1

Proving tests:

low-thresh-above-upper-lowered

A lowThresh above upperThresh is lowered to upperThresh.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.2.1
OB-UDPST: OB-UDPST resets both thresholds to their defaults.

Proving tests:

sub-interval-shortened-to-test-interval

A subIntPeriod longer than the Test Interval is shortened to it.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.2.1
OB-UDPST: OB-UDPST resets both testIntTime and subIntPeriod to their defaults.

Proving tests:

sub-interval-whole-trial-intervals

The Server coerces subIntPeriod to the nearest whole number of Trial Intervals, within its range and the Test Interval, since a Sub-Interval closes at a Status PDU; every Sub-Interval then runs its nominal length, as RFC 9097 §5.3’s equal durations need. Of those, it takes the nearest that divides the Test Interval (I = m * dt), the shorter on a tie; when the Trial Interval doesn’t divide the Test Interval none can, and the Test Interval’s last part, shorter than a Sub-Interval, is no result.

Also an OB-UDPST Difference.
RFC: RFC 9097 §5.3; RFC 9946 §7.2.1
Requirements: RFC9097-5.3-2, RFC9097-5.3-6, RFC9097-6.3-2, RFC9097-6.3-7
OB-UDPST: OB-UDPST keeps subIntPeriod and divides by each Sub-Interval’s measured time.

Proving tests:

test-interval-over-60-s

testIntTime is at most 60 s, or an Operator Key’s own maximum. Above 60 s the Server coerces useOwDelVar to RTT variation, which clock drift doesn’t distort. A Client refuses a Test Interval above the larger of its request and 60 s.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.1, §7.2.1, §7.3
Requirements: RFC9946-7.3-L1
OB-UDPST: OB-UDPST allows up to its -t maximum, 3600 s by default, and keeps useOwDelVar.

Proving tests:

congestion-inference-within-500-ms

The Server keeps slowAdjThresh × trialInt within 500 ms by lowering slowAdjThresh (to no less than 2), not trialInt. Only the Server applies the rule, since a Client doesn’t know its key’s role. Operator Keys are exempt (conformance: operator-keys-exempt-from-500-ms-congestion-inference).

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-8
OB-UDPST: OB-UDPST doesn’t limit slowAdjThresh × trialInt.

Proving tests:

dscp-from-an-allowed-set-never-ect

The Server grants dscpEcn’s DSCP only when it is in the set the key allows (only 0 for a Consumer by default; all but CS6 and CS7 for an Operator Key), and coerces another to 0. Its ECN field is always coerced to Not-ECT, since UDPSTP isn’t an ECN-capable transport.

Also an OB-UDPST Difference.
RFC: RFC 9946 §7.1, §7.2.1
Requirements: RFC9946-7.1-2
OB-UDPST: OB-UDPST allows ECT codepoints and caps the whole octet at a configured maximum (255 by default) rather than checking a set of DSCPs.
By default: on
Setting: --allowed-dscp, --operator-allowed-dscp

Proving tests:

client-refuses-a-changed-dscp

A Client accepts a Server lowering its DSCP to 0 but ends the Test if the Server changes it to another DSCP, so its traffic goes only into the class it asked for, or best effort. ECN bits in the response are ignored; the Client sends Not-ECT.

RFC: RFC 9946 §7.3
Requirements: RFC9946-7.3-L1

Proving tests:

sending-rate-table-follows-the-server-shape

The Server serves one table shape, set by its CHSR_JUMBO_STATUS and CHSR_TRADITIONAL_MTU settings (jumbo on, traditional MTU off by default) and its Server MTU (server_mtu); a Setup Request with other modifiers is refused with BADJS or BADTMTU. Rows 1 to 1000 offer N Mbps, split as OB-UDPST splits them; rows 1001 to 1152 offer OB-UDPST’s rates above 1 Gbps, with packets no bigger than the Server MTU (more, smaller packets keep the rate). Rows after the first at the top rate repeat it, and the Server goes no higher.

Also an OB-UDPST Difference.
RFC: RFC 9946 §6.1, §6.2.1
Requirements: RFC9946-6.1-8, RFC9946-6.1-9, RFC9946-6.1-11, RFC9946-6.1-12, RFC9946-6.2.1-2
OB-UDPST: OB-UDPST’s rows above 1000 use packets up to 9000 bytes when jumbo is on, whatever the path MTU.
By default: on
Setting: --jumbo, --traditional-mtu, --server-mtu
References: ADR 0004

Proving tests:

The Data Phase (32)

receive-timeouts-stop-sending-at-1s

After 1 s without Load PDUs the Load Receiver stops sending Status PDUs, and after 1 s without Status PDUs the Load Sender stops sending Load PDUs (§5.1 items 7-9). The Watchdog ends the Test Connection 3 s after the last valid PDU, §6.1’s 1 s warning plus 2 s. ADR 0002 reconciles the two sections.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.1, §6.1
Requirements: RFC9946-5.1-11
OB-UDPST: OB-UDPST follows §6.1 alone: its 1 s timer only warns and sets rxStopped, and traffic keeps flowing until its 3 s watchdog ends the Test Connection.
References: ADR 0002

Proving tests:

rx-stopped-from-half-the-timeout

rxStopped is asserted once 0.5 s, half the 1 s receive timeout, has passed without a PDU, so the PDUs sent before an end stops carry it. §8.1 recommends 1 s, which the 1 s timeouts would never let a peer see.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-L5
OB-UDPST: OB-UDPST asserts rxStopped from 1 s, and keeps sending until 3 s.
References: ADR 0002

Proving tests:

receive-timeout-is-final

Once a receive timeout has stopped an end sending, traffic that comes back neither restarts sending nor resets the Watchdog: §5.1’s “SHALL be closed” is read as final, over §8.1’s “until traffic is received again”.

RFC: RFC 9946 §5.1, §8.1
Requirements: RFC9946-5.1-13, RFC9946-5.1-16, RFC9946-8.1-4, RFC9946-8.1-12
References: ADR 0002

Proving tests:

receive-timeouts-run-from-data-phase-start

The Load PDU and Status PDU timeouts and the Watchdog run from the start of the Data Phase, so a peer that never sends stops this end after 1 s and ends the Test Connection after 3 s. The RFC doesn’t say when they start.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.1
Requirements: RFC9946-5.1-11
OB-UDPST: OB-UDPST also measures from activation, but only warns at 1 s.
References: ADR 0002

No proving test.

undefined-flags-are-invalid

A Load PDU or Status PDU whose testAction is above TEST_ACT_STOP2, or whose rxStopped isn’t 0 or 1, is invalid and counted, as OB-UDPST treats it. §8.1 defines the values but not what to do with others.

RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-3

Proving tests:

server-test-interval-timer-runs-half-a-second-over

The Server’s Test Interval timer sets TEST_ACT_STOP1 at testIntTime plus 0.5 s, as OB-UDPST’s does, so the last Sub-Interval completes: Sub-Intervals open at the first Status PDU, just after activation. §9 reads as exactly testIntTime.

RFC: RFC 9946 §9
Requirements: RFC9946-9-1

Proving tests:

stop-confirmation-waits

After STOP1 the Server keeps marking its PDUs TEST_ACT_STOP2 for one Trial Interval plus 1 s, waiting for the Client’s confirmation, then ends the Test Connection unconfirmed. The Client ends by its own Test Interval timer 1 s past the Server’s STOP1, so the Server’s STOP2 has time to cross the path.

Also an OB-UDPST Difference.
RFC: RFC 9946 §9
Requirements: RFC9946-9-2
OB-UDPST: An OB-UDPST Client ends by its own timer at testIntTime plus 3.5 s. An OB-UDPST Server in an Upstream Test sends only one STOP2 Status PDU.

Proving tests:

stop-test-action-per-role

The Client acts on TEST_ACT_STOP2 only: TEST_ACT_STOP1 is the Server’s local state, so the Client ignores it. The Server takes any valid testAction but TEST_ACT_TEST as the Client’s STOP, because an OB-UDPST Client that ends by its own timer sends STOP1.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.1, §9
Requirements: RFC9946-9-4, RFC9946-9-8
OB-UDPST: An OB-UDPST Client takes any testAction but TEST_ACT_TEST as the Server’s STOP.

Proving tests:

stop-ends-counting

The Client confirms a STOP once, with its next PDU, then ends. If STOP2 is its first Load PDU, no Status PDU timer exists yet, so it confirms at once. A STOP ends the Load Receiver’s counting, as in OB-UDPST: the Load PDU carrying it is not counted, and later ones are ignored.

RFC: RFC 9946 §9
Requirements: RFC9946-9-4

Proving tests:

stop-bursts-of-one-without-add-on

Once a Load Sender marks its Load PDUs TEST_ACT_STOP2, each Burst is one datagram with no Add-on: §9’s MAY for Bursts of one is read as one datagram in all.

Also an OB-UDPST Difference.
RFC: RFC 9946 §9
Requirements: RFC9946-9-3, RFC9946-9-6
OB-UDPST: OB-UDPST cuts the Burst to one datagram but keeps the Add-on.

Proving tests:

client-initiated-stop

A Client that ends a Test early (a Coercion it refuses, or DataPhase::stop) sends one PDU marked TEST_ACT_STOP2 at once and ends, within 1 s if the PDU can’t go out, so the Server ends its Test Connection at once instead of by its Watchdog. §9 defines only a Server-initiated stop. A STOP that reaches the Server before its own STOP1 ends the Test Connection as StoppedByClient; an on-path attacker setting STOP (§11) looks the same.

RFC: RFC 9946 §9, §11

Proving tests:

server-initiated-stop

When its administrator stops the Server, each active Test Connection makes a final send attempt marked TEST_ACT_STOP2 at once and ends as Stopped, without waiting for confirmation. As Load Sender, its Send Plan includes any Deferred Datagrams. If the PDU cannot be handed to the runtime, the Data Phase ends within 1 s. An end already silenced by its receive timeout sends nothing. This lets a reachable Client complete its Test instead of ending by its Watchdog. Section 9 defines the timer-triggered STOP exchange but not an administrator’s shutdown; its ordinary timer-driven behavior is unchanged.

RFC: RFC 9946 §5.1, §9
References: ADR 0014

Proving tests:

random-size-flag

OB-UDPST’s random-size flag (0x80000000 in a size field), which RFC 9946 doesn’t define, is honoured: a size is drawn once per Burst, uniformly from 52 bytes (32 over IPv6) up to the field’s low 31 bits, as OB-UDPST does. A random field whose maximum is 0 sends no datagram. OB-UDPST’s row 0 sets it on the Add-on of every Upstream search.

RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-6

Proving tests:

sending-rate-structure-only-from-test-status-pdus

The Client takes a new Sending Rate Structure only from Status PDUs marked TEST_ACT_TEST, as OB-UDPST does: a STOP Status PDU’s needn’t be one to send at.

RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-6

Proving tests:

lpdu-time-per-burst

Every Load PDU of a Send Plan carries the same lpduTime, read once just before the headers are written. lpduTime is the Test Time Base plus a wall-clock offset fixed at activation, so wall-clock steps don’t move it. OB-UDPST also stamps once per Burst, from its cached loop clock.

RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-L10
References: docs/spec.md Time model

Proving tests:

deferred-datagrams-and-withheld-bursts

Datagrams of a Send Plan the host didn’t take are Deferred Datagrams: they lead the next Send Plan (a STOP one included) with their sequence numbers and a fresh lpduTime, so host backpressure never shows as loss. A failed send defers the rest of its Send Plan. A new Burst is a Withheld Burst, counted and reported (DataEvent::BurstsWithheld), while at least as many Deferred Datagrams as it holds are waiting or it would overflow the Send Plan. The RFC doesn’t say what a Load Sender does with datagrams the host refuses.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.1
OB-UDPST: OB-UDPST drops the datagrams the host refused and, with seqNumAdjust, rolls lpduSeqNo back so they don’t count as loss.
References: ADR 0008

Proving tests:

sub-interval-boundaries

A Sub-Interval opens when the first Status PDU is sent. It closes just after the first Status PDU sent more than subIntPeriod minus trialInt/2 after it opened (whole ms, strict >), so the next Status PDU reports it. Until one has completed, subIntSeqNo is 0 and sisSav is zero. accumTime adds up each Sub-Interval’s duration rounded to ms. The RFC is silent on these; they are OB-UDPST’s, so both report the same Sub-Intervals.

RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-L3, RFC9946-8.2-L11

Proving tests:

status-pdu-grid-from-first-load-pdu

Status PDUs stay on a Trial Interval grid from the first Load PDU received, so a late one doesn’t move the later ones, and the first tiDeltaTime is measured from the first Load PDU.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-1
OB-UDPST: OB-UDPST re-arms its Status PDU timer after each send, and measures the first tiDeltaTime from activation.

Proving tests:

received-bytes-from-datagram-length

The Load Receiver counts each Load PDU’s bytes from the datagram’s actual length, which the runtime knows even when it reads only the header, not from its udpPayload field, which §8.1 says allows accurate counts under receive truncation. A peer can’t inflate the results by stating a larger udpPayload.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.1
Requirements: RFC9946-8.1-L7
OB-UDPST: OB-UDPST sums the udpPayload fields.

Proving tests:

status-pdu-auth-fields-ignored-outside-mode-2

Outside Authentication Mode 2 a Load Sender doesn’t check a Status PDU’s keyId, authUnixTime and authDigest, which the RFC has zero there; only mode 2 authenticates Status PDUs. udpstp sends them zero.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.3.2, §6.2
Requirements: RFC9946-5.3.2-6
OB-UDPST: OB-UDPST carries other data in these fields.

Proving tests:

sequence-errors-count-load-pdus

Sequence Errors count Load PDUs, not events: a gap of n counts n lost. The Load PDUs before the first one received count as lost, none if that first lpduSeqNo is 0 or more than 1024 from 1.

RFC: RFC 9946 §8.2

Proving tests:

late-load-pdu-decrement-in-its-own-interval

An out-of-order Load PDU takes one off the loss count, as OB-UDPST does, but only in a Trial Interval or Sub-Interval that counted it as lost.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-L9
OB-UDPST: OB-UDPST takes it off whatever interval is open, which can cancel an unrelated loss.

Proving tests:

lpdu-seq-no-modulo-2-32

lpduSeqNos are compared modulo 2^32, so counting carries on across the wrap from 4294967295 to 0. A Load PDU exactly 2^31 ahead counts as late.

Also an OB-UDPST Difference.
RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-L9
OB-UDPST: OB-UDPST doesn’t handle the wrap.

Proving tests:

ms-rounding-half-up

A span in ms is rounded half up with floor division (−0.5 ms is 0, −0.5000001 ms is −1), per Load PDU, before the minimum and the subtraction, as OB-UDPST’s tspecmsec does. §8.2 doesn’t say how to round.

RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-L10

Proving tests:

clock-delta-min-encoding

clockDeltaMin is sent as a two’s-complement 32-bit integer, as OB-UDPST sends it, and never read as “no value” (−1 is 0xFFFFFFFF). It saturates at the 32-bit limits in ms, and is 0 until the first Load PDU. The RFC gives no encoding for a negative value.

RFC: RFC 9946 §8.2
Requirements: RFC9946-8.2-L14

Proving tests:

delay-samples-from-in-order-load-pdus

Only in-order Load PDUs give delay and RTT samples, and the first one only seeds clockDeltaMin, as in OB-UDPST.

RFC: RFC 9946 §8.2

Proving tests:

wall-clock-step-drops-delay-samples

When the clock pair around a read disagrees (the wall clock stepped during it), that read’s delay and RTT samples are dropped and counted (DataEvent::DelaySamplesDropped): docs/spec.md’s “flagged” read as not trusted.

RFC: RFC 9946 §8.2
References: docs/spec.md Time model

Proving tests:

kernel-receive-timestamp-clamped

A kernel receive timestamp, a Load PDU’s or a Status PDU’s, is kept no later than the read, and no earlier than the 1 s receive timeout before it, since a datagram may have waited in the socket buffer.

RFC: RFC 9946 §8.1, §8.2
References: docs/spec.md Time model

Proving tests:

rtt-sample-rules

A Load PDU gives an RTT sample only when it echoes a new spduTime; spduTime 0 has nothing to echo. The RTT is the receive time minus spduTime minus rttRespDelay. An rttRespDelay one more than the raw RTT is rounding and gives 0, so rttMinimum may be 0 (rtt-minimum-may-be-zero); a larger one leaves the RTT as it is. Both as in OB-UDPST.

RFC: RFC 9946 §8.1, §8.2
Requirements: RFC9946-8.1-L12, RFC9946-8.2-L15

Proving tests:

zero-checksum-sent-as-ffff

A computed checksum of 0 is sent as 0xFFFF, its equal in one’s complement, so a populated field never reads as unpopulated. This applies to every PDU udpstp fills a checkSum in. §5.6 gives no substitution rule.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.6
Requirements: RFC9946-5.6-3
OB-UDPST: OB-UDPST sends 0, which its peer reads as unpopulated.

Proving tests:

broadcast-means-limited-broadcast

“Broadcast” is 255.255.255.255, also in its IPv4-mapped IPv6 form; multicast is 224.0.0.0/4 and ff00::/8. A subnet’s directed broadcast address can’t be told without its prefix length, which the core doesn’t know, and deprecated IPv4-compatible IPv6 addresses aren’t looked into. Activation Requests from a multicast or broadcast source are dropped too (docs/spec.md story 89).

RFC: RFC 9946 §6
Requirements: RFC9946-6-1, RFC9946-6-2

Proving tests:

windows-dscp-coerced-to-0

Where test traffic can’t be marked (Windows, which reserves the DS field for its QoS API), a Server allows only DSCP 0, coercing any other dscpEcn to it, and a Client asks for 0, each with a warning. dscpEcn is read as the marking the Test’s traffic actually carries, so the Activation Response never states one that isn’t sent; §7.2.1 lets the Server coerce dscpEcn.

RFC: RFC 9946 §7.1, §7.2.1
Requirements: RFC9946-7.2.1-L1
References: docs/technology.md

Proving tests:

Keys and authentication (8)

kdf-appendix-a-layout

The KDF’s fixed input is laid out as RFC 9946 Appendix A’s OpenSSL call builds it: BE32(counter), “UDPSTP”, a zero byte, the decimal authUnixTime of the first Setup Request, BE32(512). §5.4.1’s prose fits at least five layouts; Appendix A is the only one the RFC pins down, and OB-UDPST uses it too. The Shared Key goes in as raw bytes, never cut at a zero byte as Appendix A’s strlen(Kin) would; for text keys the two agree.

RFC: RFC 9946 §5.4.1
Requirements: RFC9946-5.4.1-3
References: ADR 0001

Proving tests:

key-table-lifetimes

The Server finds a key by keyId among the rows whose accept lifetime includes its own wall clock; a keyId with rows but none in its accept lifetime is an authentication failure. The Client sends with the row whose send lifetime includes its wall clock, preferring the newest SendLifetimeStart. Bounds are inclusive and no clock-skew grace is added. Rows may share a keyId only if neither their accept nor their send lifetimes overlap, so a keyId always selects at most one row. Established Test Connections keep their Derived Keys when a lifetime ends.

RFC: RFC 9946 §5.4, RFC 7210 §3
Requirements: RFC9946-5.4-3
References: ADR 0010

Proving tests:

key-table-alg-id

A Key Table row’s KDF is HMAC-SHA-256, its IANA “KeyTable KDFs” name, standing for §5.4.1’s counter-mode construction. The “KeyTable AlgIDs” registry has no value for the authDigest algorithm, so the AlgID is the unregistered name HMAC-SHA-256, mirroring the KDF. Both may be left out, and no other value is accepted. LocalKeyName is the keyId.

RFC: RFC 9946 §5.4, RFC 7210 §2
Requirements: RFC9946-5.4-3, RFC9946-5.4-4
References: ADR 0010, docs/spec.md

Proving tests:

key-table-operator-extension

A TOML Key Table row may carry operator = true, making it an Operator Key, and an Operator Key may carry max_test_interval, its own maximum Test Interval of at least 60 s. RFC 7210 has no column for either; RFC 9946 §5.4 asks for “(at least)” its fields, so these are extensions of our key file format. Operator-only Fixed-Rate Tests and Tests longer than 60 s need them.

Also an OB-UDPST Difference.
RFC: RFC 9946 §5.4, RFC 7210 §2
Requirements: RFC9946-5.4-3
OB-UDPST: OB-UDPST key files have only a keyId and a key: no role and no per-key Test Interval.
References: ADR 0010, docs/spec.md

Proving tests:

key-lifetime-format

Lifetimes are written YYYYMMDDHHMMSSZ, in UTC, as RFC 7210’s prose describes (year, month, day, hour, minute, second), although its format line says YYYYMMDDHHSSZ. Leap seconds (second 60) and years before 1970 are refused.

RFC: RFC 7210 §2
References: ADR 0010

Proving tests:

key-text-extension

A TOML Key Table row gives its key as exactly one of key_hex or key_text. key_hex accepts upper- and lowercase digits; key_text is an extension that takes the key as its UTF-8 bytes, so a text key shared with OB-UDPST needs no conversion. RFC 7210 writes Key as a lowercase hexadecimal string.

RFC: RFC 7210 §2
References: docs/spec.md

Proving tests:

several-valid-keys-need-key-id

When several rows are valid for sending now and tie for the newest SendLifetimeStart (every row of an OB-UDPST key file, which has no lifetimes), the Client doesn’t guess: it doesn’t start unless --key-id pins one. RFC 7210 §3 selects by the newest start and leaves a tie open.

Also an OB-UDPST Difference.
RFC: RFC 7210 §3
OB-UDPST: OB-UDPST’s Client uses keyId 0 (DEF_KEY_ID) unless -y names another, or the only key when the file has one.
By default: on
Setting: --key-id
References: ADR 0010

Proving tests:

derived-keys-depend-only-on-the-row-and-time

The Derived Keys are the KDF of a Key Table row’s Shared Key and the first Setup Request’s authUnixTime, nothing else, so the Server keeps them by row and time and reuses them for the next request with both (a flood costs one KDF run). A Setup Request whose authDigest fails leaves nothing of its own: it is dropped, admits nothing, and no key from it is used for it; a later request with a valid digest would derive the same keys.

RFC: RFC 9946 §5.4.1
Requirements: RFC9946-5.4.1-7

Proving tests:

OB-UDPST Differences

An OB-UDPST Difference is behaviour in which udpstp and OB-UDPST differ while udpstp conforms to the RFC. 31 records are OB-UDPST Differences only; with the Interpretations OB-UDPST doesn’t share, 97 records describe a difference from OB-UDPST.

Load Rate Adjustment Algorithms (1)

delay-var-is-the-maximum-by-default

With useOwDelVar True, a Trial Interval’s DelayVar is its maximum one-way delay variation (delayVarMax), the “delay range” RFC 9946 §5.1(6) and RFC 9097 §8.1 judge. A Server setting makes it the average, delayVarSum / delayVarCnt rounded half up, as OB-UDPST uses. With useOwDelVar False it is the Status PDU’s rttVarSample, as in OB-UDPST.

RFC: RFC 9946 §5.1; RFC 9097 §8.1
OB-UDPST: Classifies by the rounded average one-way delay variation, which is lower than the maximum, so an OB-UDPST Server sees fewer Errored Intervals and can search higher on a jittery path.
By default: on
Setting: --delay-var-statistic, delay_var_statistic in TOML

Proving tests:

The Client (9)

auth-mode-2-default

The Client authenticates in mode 2 by default, which also authenticates every Status PDU. Mode 1 is used only when chosen, for all Servers or per Server, never as a fallback.

RFC: RFC 9946 §5.3
OB-UDPST: OB-UDPST supports mode 1 only: its Client sends mode 1 with a key, and 0 without one.
By default: on
Setting: --auth-mode, --mode-1-server
References: docs/spec.md

Proving tests:

exit-zero-with-warnings

A Test that completed with only warnings exits 0, so scripts treat it as a completed measurement. The JSON ErrorStatus still carries the warning.

OB-UDPST: OB-UDPST exits with the warning’s ErrorStatus.
References: docs/spec.md story 31

Proving tests:

exit-own-error-statuses

udpstp adds ErrorStatus values OB-UDPST lacks, in numbers it leaves unused: 49, the last warning code, for a completed Test with no valid Maximum IP-Layer Capacity (JSON only); 201 for a coercion the Client was told to refuse; 202 for a parameter the Client can’t run with; 203 for a Multi-Connection Test whose Servers set different parameters; 204 for a Test the Client user interrupted (record client-interrupt). These ErrorStatus values sit in OB-UDPST’s connection-error range (200 plus an offset), away from the ranges that add an IANA-registered cmdResponse.

OB-UDPST: OB-UDPST has none of these: it adds no warning for a missing maximum, and uses only 200 in its connection-error range.
References: docs/spec.md

Proving tests:

client-interrupt

On Ctrl-C (SIGINT), SIGTERM or SIGHUP (each unless the Client started with it ignored), or the Windows console’s Ctrl-C, Ctrl-Break, close and shutdown events, while its Test runs, the Client stops the Test: each Test Connection in its Data Phase sends its STOP2 (record client-initiated-stop), so the Server ends its own at once, within 1 s, and the results so far are printed with ErrorStatus 204. On Unix the Client then dies of the same signal, so a shell sees 128 plus its number, even when every Test Connection had already ended (the JSON ErrorStatus then reflects the Test); on Windows it exits 204. A signal before the Test starts or after it ends, or a second one, ends the Client at once. A failed write to standard output is noticed when it fails: it stops a running Test the same way, and the Client exits 204, as when its final results can’t be written.

OB-UDPST: OB-UDPST on SIGINT or SIGTERM stops sending without a STOP, so the Server ends the Test by its Watchdog, and exits 255 (udpst.c signal_exit).

Proving tests:

exit-most-severe-warning

A completed Test reports its most severe warning, in this order: 49 no valid maximum, 4 traffic stopped for 1 s, 5 the peer stopped receiving, 3 the peer missed Status PDUs, 2 Status PDUs never arrived, 6 invalid PDUs. A warning never replaces an error. A Multi-Connection Test reports success with the most severe warning of all once any Test Connection completed; otherwise the first Test Connection’s error by mcIndex, or 200 when none had one of its own.

OB-UDPST: OB-UDPST keeps the last status written: a later warning replaces an earlier, more severe one, and replaces an error too; 200 overwrites only success or a warning, and a Test Connection’s normal stop turns any error into 0.
References: docs/spec.md

Proving tests:

json-at-max-is-the-maximum

In the JSON results, AtMax and ModalResult describe the Maximum IP-Layer Capacity, the highest Sub-Interval that meets the Performance Criterion, as RFC 9097 defines the metric. A Capacity Mode with no such Sub-Interval has no AtMax (or ModalResult entry) and ErrorStatus 49. The Highest IP-Layer Capacity, with no loss criterion, is in the udpstp extension object.

RFC: RFC 9097 §6.3
Requirements: RFC9097-6.3-L1
OB-UDPST: OB-UDPST’s AtMax is the Highest IP-Layer Capacity: the Sub-Interval with the greatest rate, whatever its loss.

Proving tests:

json-summary-delays-from-samples

In the JSON results, the delay fields of a Sub-Interval without delay or RTT samples are 0, as OB-UDPST writes them, and the Sub-Interval is listed under NoSamples in the extension. Summary and ModalSummary are computed from real samples only.

OB-UDPST: OB-UDPST’s Summary.PDVMin and RTTMin take the minimum over Sub-Intervals including those zeros, and PDVAvg averages them in.

No proving test.

json-missing-sub-intervals

In the JSON results, a Sub-Interval never measured is left out of IncrementalResult and listed under MissingSubIntervals in the extension, and Interval is each Sub-Interval’s subIntSeqNo, so a gap shows.

OB-UDPST: OB-UDPST’s Interval counts the Sub-Intervals processed, so it has no notion of a missing one: the numbering runs on.

No proving test.

json-incomplete-test-results-in-extension

When a Test activated but didn’t complete (ErrorStatus 200 and up), Output has only its header and trailer, as OB-UDPST writes it. The extension’s SubIntervals keeps every measured Sub-Interval with the same fields and units as Output.IncrementalResult, including loss and delay figures, or an empty array if none was measured. Its MinRTT is the smallest measured minimum RTT in seconds, or null if unavailable. Both are omitted with --json-extension=false.

OB-UDPST: OB-UDPST drops every Sub-Interval result of a Test that ends with ErrorStatus 200.

Proving tests:

The Control Phase and Admission (7)

silent-authentication-failures

Every Setup Request that fails authentication is dropped silently (bad digest, unknown keyId, key outside its lifetime, time outside the window, unsupported or missing authMode, no keys configured), as the RFC’s default; only Troubleshooting Mode answers.

OB-UDPST: OB-UDPST always answers these failures with a coded Setup Response (AUTHFAIL, AUTHTIME, AUTHNC, AUTHREQ, AUTHINV) and has no silent mode.

Proving tests:

legacy-protocol-versions-get-badver

Any protocolVer other than 20 is rejected with BADVER, legacy versions 11 to 19 included; the response carries protocolVer 20.

OB-UDPST: OB-UDPST also serves protocolVer 11 to 19.

Proving tests:

setup-check-order

The Server checks a Setup Request in the RFC’s order: size, checksum, authentication, then the replay cache, the Source’s budget and the Key Table row’s budget, then the other fields: BADVER, BADJS, BADTMTU. Then Admission’s limits: a copy of an admitted request is ignored; the Multi-Connection fields (MCINVPAR); for a new Test, the Tests per Source, then the Quota (CONNFAIL); a missing maxBandwidth (NOMAXBW); the per-Test cap, then the global cap (CONNFAIL); the Admitted Rate Cap and one Test’s share of it (CAPEXC).

OB-UDPST: OB-UDPST judges the fields before authMode, and checks MCINVPAR right after BADVER, before BADJS and BADTMTU; so an unauthenticated request learns the field results before being told authentication is required.

Proving tests:

unknown-rate-adj-algo-refused

An Activation Request whose rateAdjAlgo is neither B nor C is refused with BADPARAM: running another algorithm would measure by a method the Client didn’t ask for.

OB-UDPST: OB-UDPST coerces any other value to B. Its Client only sends B or C, so interop isn’t affected.

Proving tests:

dual-phase-reserved-field-ignored

The Activation Request’s reserved fields are ignored, as the RFC requires, including the one OB-UDPST uses for srAdjSuppCount (dual-phase testing), so a Consumer can’t get a fixed rate that way.

OB-UDPST: OB-UDPST reads srAdjSuppCount from a reserved field and runs row 0 or the requested row as a fixed rate for the suppressed Sub-Intervals.

Proving tests:

out-of-range-parameters-clamped

An out-of-range Activation parameter is clamped to the nearest limit of our range, and the response reports it. Our ranges are narrower than OB-UDPST’s. An OB-UDPST Client adopts the coerced values without complaint.

OB-UDPST: OB-UDPST resets an out-of-range value to its default (useOwDelVar to False, ignoreOooDup to True). Its ranges are wider: thresholds 1 to 10000, highSpeedDelta and slowAdjThresh from 1, seqErrThresh to 65535, testIntTime to 3600.

Proving tests:

row-zero-fixed-size

Row 0 sends one 625-byte IP packet every 50 ms on the second transmitter: 0.1 Mbps, always the same size. The RFC gives row 0 no meaning beyond the first row.

OB-UDPST: OB-UDPST’s row 0 sends one add-on every 50 ms of random size, up to 1222 bytes of payload (1472 with -T), with the random-size bit set in udpAddon2: about 0.11 Mbps.

Proving tests:

The Data Phase (10)

peer-tears-down-a-second-later

An end that stopped sending also stops refreshing its peer’s Watchdog. After a break in one direction, an OB-UDPST peer ends its Test Connection about 1 s after udpstp does, and reports traffic stopped in both directions.

OB-UDPST: OB-UDPST keeps sending for 3 s, so its peer’s Watchdog was last reset about 1 s later than ours.
References: ADR 0002

No proving test.

stop-pdus-reset-the-watchdog

Every valid Load PDU or Status PDU resets the Watchdog, STOP ones included, as the RFC describes the reset.

OB-UDPST: OB-UDPST resets its watchdog only while both its own and the received testAction are TEST_ACT_TEST.

No proving test.

transmitter-switched-on-starts-at-once

A Transmitter that a new Sending Rate Structure switches back on starts at once.

OB-UDPST: OB-UDPST arms it only when the other Transmitter fires, so once both are zeroed the Client never sends again.

Proving tests:

stop-confirmed-without-a-transmitter

An Upstream Client whose Sending Rate Structure has no Transmitter still confirms a STOP, with a header-only Load PDU.

OB-UDPST: An OB-UDPST Client confirms at its next send timer, so with no Transmitter armed it sends nothing to confirm with.

Proving tests:

server-polices-upstream-rate

The Server polices an Upstream Client: three Trial Intervals in a row above 1.5 times the highest rate it assigned within the last second start the STOP exchange early and end the Test Connection with EndReason::OverRate (ADR 0008, amended by ADR 0011 so a search through a deep buffer isn’t stopped).

OB-UDPST: OB-UDPST doesn’t police the Client’s rate.
References: ADR 0008, ADR 0011

Proving tests:

missed-bursts-catch-up-or-are-skipped

The Load Sender keeps an absolute schedule. After a late wake-up it still sends each missed Burst less than the catch-up budget (1 ms; 2 ms on Windows, whose timers wake later) late, and the most recent one always; older ones are Skipped Bursts, counted, reported (DataEvent::BurstsSkipped) and deducted from the IP-Layer Sender Bit Rate. ADR 0008 and ADR 0012 hold the reasoning.

OB-UDPST: OB-UDPST re-arms each send timer from “now”, so missed intervals are lost with no catch-up and no record.
References: ADR 0008, ADR 0012

Proving tests:

status-pdu-sums-saturate

tiRxBytes, delayVarSum and the RTT variation sums saturate at their field’s maximum. Only extreme byte counts or lpduTimes reach this.

OB-UDPST: OB-UDPST’s wrap.

No proving test.

lpdu-seq-no-jumps-held

An lpduSeqNo jump of more than 1024 (for a first Load PDU, more than 1024 from 1 either way) counts only once the next Load PDU continues from it within 1024; otherwise it is ignored (DataEvent::IgnoredLoadPdus). 32 out-of-order Load PDUs in a consecutive run more than 1024 behind resync counting to them, and open intervals then take back their own loss. The held Load PDU gives no delay sample and counts in the interval of the Load PDU that confirms it. Load PDUs carry no authentication, so this keeps a forged one from hiding loss.

OB-UDPST: In OB-UDPST one forged Load PDU up to 2^31 ahead leaves every later one out of order, hiding all loss for the rest of the Test.

Proving tests:

negative-rtt-gives-no-sample

A negative raw RTT, from an echoed spduTime in the future, which only a forged or corrupted Load PDU carries, gives no sample.

OB-UDPST: OB-UDPST casts it to unsigned and reports a huge rttVarSample.

Proving tests:

checksum-verified-only-when-configured

A populated checkSum is verified only when checksum use is configured, as §5.6 says.

OB-UDPST: OB-UDPST verifies every populated checkSum, whatever its build flag.

Proving tests:

Keys and authentication (4)

obudpst-key-file-reading

An OB-UDPST udpst.keys file is read as OB-UDPST reads it (comma, space and tab separators; a line ends at #, \r or a NUL byte; keyIds as C’s strtol reads them; keys of up to 64 bytes), with two exceptions. Lines aren’t split at 1023 bytes. A keyId with a - sign, or one above 255 before narrowing, is refused.

OB-UDPST: OB-UDPST reads lines with fgets into a 1024-byte buffer, so a longer line continues as a new one, and narrows strtol’s result to int, so a signed or oversized keyId can wrap into 0-255.

Proving tests:

key-file-private

On Unix unsafe key-file permissions are refused. Windows assesses and reads the same read-shared file under ADR 0015’s supported local-NTFS policy, trusting only the running account, SYSTEM and built-in Administrators. Only a supported policy match proceeds to content reading; mismatches and unverified permissions are distinct key file errors (51), with accurate reasons and remediation, without claiming effective exposure. Server errors reach stderr and any configured startup log; Client failures leave stdout empty. The native unsafe boundary, descriptor bounds, read-only sharing and content wiping remain unchanged. The settings-only preview loads no keys; ordinary print-config still validates them. The manual reconstruction removes explicit outsider grants. The installer validates keys through a real virtual-account trial and contains failures with disabled startup, stopped service and cleared recovery. Protected containing storage remains a deployment assumption. RFC 9946 prescribes no storage algorithm.

OB-UDPST: OB-UDPST reads a key file whatever its permissions.
References: docs/spec.md, ADR 0015

Proving tests:

key-file-owner

On Unix a key file must be owned by the effective user running udpstp or by root; another owner could replace the keys. Running udpstp setuid isn’t supported.

OB-UDPST: OB-UDPST doesn’t check who owns a key file.

Proving tests:

key-file-size-limit

A key file larger than 1 MiB is refused, so it can be read into one buffer that is wiped after parsing.

OB-UDPST: OB-UDPST reads a key file of any length, line by line.

Proving tests:

Conformance Records

cargo xtask req checks these files, prints coverage per section and each record’s proving tests, and writes both to summary.md. It also writes interop-masks.toml. CI fails if either committed file is stale.

Requirements

requirements/ holds one TOML file per top-level RFC section, for example rfc9946-06.toml for RFC 9946 §6. Each normative statement is one entry:

[[requirement]]
id = "RFC9946-6.1-4"
text = "Note: Any field labeled as 'reserved for alignment', in any PDU, MUST be set to 0 and MUST be ignored upon receipt."
text_sha256 = "…"          # `cargo xtask req hash` < text
state = "pending"          # pending | deviation | interpretation | operational | not-applicable
reference = "ADR 0002"     # required for deviation and interpretation: an ADR or a record ID, or a list of them
ticket = 156               # required while pending and untested: the issue expected to test it, or a list
nightly = true             # optional: tested only by tests that run nightly
note = "…"                 # optional
  • ID: <document>-<subsection>-<ordinal>. The ordinal counts the sentences in that subsection that contain a BCP 14 keyword (MUST, SHALL, SHOULD, MAY, REQUIRED, RECOMMENDED, OPTIONAL, in capitals), in the order they appear, starting at 1. IDs never change once assigned.
  • Lowercase statements: a sentence without a BCP 14 keyword is also a Requirement when it prescribes what an endpoint sends, sets or does, or permits it something (“has to be set”, “set to”, “may restrict”, “is recommended”, “the client ends the test”, “the OPTION to coerce”). Pure definitions of a field’s meaning or unit, and restatements of a keyword sentence, are not. These have their own ordinal series with an L prefix, for example RFC9946-7.2.1-L1, counted in text order when the subsection is extracted; one added later takes the next free number.
  • Text: the sentence quoted exactly from the RFC. Line breaks don’t matter; the check collapses whitespace before comparing with the RFC text in docs/rfc/.
    • A sentence that introduces a list, or has a list inside it, is quoted with the list, because the list completes it. A sentence that introduces a figure or code ends at the colon.
    • A bullet that is a sentence of its own, with its own keyword, gets its own ID and is quoted without its bullet marker.
    • Field labels such as srStruct: are left out; a leading Note: is kept.
  • Hash: the first 16 hex digits of SHA-256 over the whitespace-collapsed text. It catches accidental edits.
  • State: “tested” is never written here. A pending Requirement counts as tested when a test that runs on the gating platform carries its tag.
  • Ticket: a pending Requirement no test covers names the issue, or issues, expected to test it; the summary lists them by ticket. Once it is tested, its ticket goes.
  • Gating platform: Linux, which CI runs. A test counts there unless it is #[ignore]d or its #[cfg], or its file’s #![cfg], excludes Linux.
  • Nightly: a Requirement whose only tests are ignored, because they run in the nightly workflow, is classified nightly = true and counted as tested nightly, apart from the rest.

Records

records/ holds the Conformance Records: every Deviation, Interpretation and OB-UDPST Difference (see GLOSSARY.md), one TOML file per area of behaviour. Each record is one behaviour:

[[record]]
id = "tests-per-source"         # kebab-case, unique, never reused
kind = "deviation"              # deviation | interpretation | obudpst-difference
deviation = "non-conformance"   # a Deviation's kind: non-conformance | interop-accommodation | platform-gap | unmet-should
summary = "…"                   # what udpstp does, and why
rfc = "RFC 9946 §4.2"           # where the Requirement or ambiguity is; optional for an OB-UDPST Difference
requirements = ["RFC9946-…"]    # optional: the Requirement IDs it concerns
obudpst = "…"                   # what OB-UDPST does, when it differs
default = "off"                 # on | off: for a Deviation, and whenever there is a setting
setting = "`--tests-per-source`, `tests_per_source` in TOML"
revisit = "…"                   # for a Deviation: what would make it worth revisiting
reference = ["ADR 0002", "docs/spec.md story 32"] # Durable public sources
tests = ["crates/udpstp-core/tests/tests_per_source.rs::…"]
masks = [{ pdu = "activation-response", fields = ["sr_index_conf"], when = ["max-bandwidth-stated"] }]
  • Kinds: an Interpretation that OB-UDPST doesn’t share is one interpretation record with obudpst; it counts as an OB-UDPST Difference too. A record whose ADR holds the reasoning keeps the summary short and names the ADR.
  • Deviations and Requirements: a Deviation that lists a Requirement makes that Requirement’s state deviation, with the record’s ID in its reference (a list when several Deviations depart from it), and the other way round. An Interpretation or OB-UDPST Difference may list Requirements without changing their state: a Requirement it reads is met as read, and counts as tested once a test carries its tag, as any other.
  • Proving tests: path/to/file.rs::test_name. The report says for each whether it runs, runs only under a cfg, is ignored, isn’t a test or is missing. A record none of whose tests runs is unproven and listed in the summary.
  • Code notes: code that implements a record says so in a comment ending (conformance: <id>); the reason lives in the record.
  • Masks: a difference from OB-UDPST that shows in a Control PDU udpstp sends (setup-request, setup-response, activation-request, activation-response, null-request) names the fields, as udpstp-wire names them (sending_rate.udp_payload1 for a nested one), and the conditions under which it shows, from CONDITIONS in xtask/src/records.rs. interop-masks.toml lists every masked field with its conditions and records; the OB-UDPST interop comparison ignores those fields in runs where the conditions hold. Per-run fields (authUnixTime, authDigest, ports, mcIdent) and the checksum, which follows the other fields, are the comparison’s own business. OB-UDPST’s uses of reserved fields are checked separately, by the check that they stay zero.

Tags

A test proves Requirements with the #[req] attribute from the udpstp-req crate (a dev-dependency):

#[udpstp_req::req("RFC9946-6.1-4", "RFC9946-6.1-13")]
#[test]
fn reserved_field_is_ignored_on_receipt() { … }

Every normative statement of RFC 9946 and of the RFC 9097 sections it imports has an ID. A test for something without one (the specification, an ADR, a Conformance Record) names it in a plain comment. The check still reports // req: section markers, the form tests used before the IDs existed; none are left.

What fails the check

  • Requirements: a tag naming an unknown ID, a text whose hash doesn’t match, a text not found in the RFC, a duplicate ID, a deviation or interpretation without a reference, and a reference that names neither an ADR nor a record of the same kind that lists the Requirement.
  • Completeness: a BCP 14 keyword, in any subsection of a section that has a requirements file, outside every requirement’s text (code blocks and table lines aside; a text the RFC repeats covers each place); a keyword series whose ordinals don’t follow the text. The L series isn’t checked for order, since one found later takes the next free number.
  • Classification: a pending Requirement with no test and no ticket; a tested one that still names a ticket; one whose only tests don’t run on the gating platform and isn’t nightly; nightly on one that a gating test covers, or that no test covers; a #[req] tag on a function that isn’t a test.
  • Records: a duplicate ID; a Deviation without its kind, default or revisit trigger; a Deviation or Interpretation without rfc; a setting without a default, or a --flag the binary doesn’t have; an unknown Requirement ID or ADR; a Deviation whose Requirement isn’t marked; a listed test that is missing or isn’t a test; a mask without obudpst, or naming an unknown field or condition; a conformance: note naming no record.

Unproven records are reported but don’t fail the check yet.

Security policy

Reporting a vulnerability

Please report vulnerabilities privately through GitHub’s private vulnerability reporting, never in a public issue.

Include what an attacker can do, the steps or PDUs that reproduce it, and the version or commit. The maintainer aims to acknowledge a report within a week, agree a fix and a disclosure date with you, and credit you in the advisory unless you prefer otherwise.

Supported versions

The latest published release receives security fixes. Update to that release when reporting a vulnerability, or include the version affected if you cannot update.

Scope

docs/security.md describes the threat model: what udpstp defends against, what is planned, and what is out of scope. Examples of what counts:

  • A way to make a Server or Client send load to a third party.
  • Bypassing authentication, the replay protection or the Operator-only rules.
  • A crash, hang or resource exhaustion triggered by crafted PDUs.
  • Leaking key material.

Weaknesses of the protocol itself, such as the lack of encryption, are out of scope here. You can raise them as public issues.

Architecture

This document describes how udpstp is built: the parts of the system, the crates, the line between protocol logic and I/O, and how a Test Connection moves through its Control Phase and Data Phase. It describes the code on main. Anything not built yet is marked planned, with its issue number.

Terms are used as GLOSSARY.md defines them. Decisions are recorded in docs/adr/.

1. System context

A Client and a Server run the same udpstp binary (udpstp client, udpstp server). The Client sends a Setup Request to the Server’s Standard Port (24601). The Server answers from the Standard Port, and every later PDU of the Test Connection uses the Ephemeral Port it allocates, by default from 49152-65535.

The system. A udpstp Client and a udpstp Server each read a key file, a TOML Key Table or an OB-UDPST udpst.keys file, and the Server also reads its TOML configuration. The Client sends a Setup Request to the Standard Port, 24601, and then exchanges the Activation exchange, Load PDUs and Status PDUs with the Server over the Ephemeral Port, across the network path under test. OB-UDPST's Client and Server are shown as interop peers.
  • Key files. Both ends need the same Shared Key. A file whose name ends in .toml is read as udpstp’s Key Table; any other file is read as an OB-UDPST udpst.keys file (crates/udpstp/src/keyfile.rs). Keys are never given on the command line. Unix refuses unsafe ownership and group/other content access. Windows inspects and reads the same handle with read-only sharing; supported-policy mismatches and unverified permissions are refused before reading keys, with distinct key file errors (ADR 0015).
  • Server configuration. Built-in defaults, then the TOML file (--config), then command-line options, each overriding the one before. --print-config prints the result (crates/udpstp/src/config.rs). Hosting a Server lists every setting and its default.
  • Client options. --direction, --test-interval, --algorithm, --fixed-rate or --start-row, --max-bandwidth, --delay-var, --random-payload, --refuse-coercion (RFC 9946 parameter names), --checksum, --key-file, --key-id, --unauthenticated, --time-limit, --test-connections, --min-test-connections, --auth-mode (2 by default), --mode-1-server, --dscp, --jumbo, --traditional-mtu, --max-hops, --dont-fragment, --socket-buffer-size, the results’ --format, --json-extension, --max-loss-ratio and --first-mode-sub-intervals (crates/udpstp/src/main.rs). Rows are 0 to 1152. The Trial Interval, Sub-Interval, thresholds and other Activation parameters take the parameter table’s defaults (crates/udpstp-core/src/parameters.rs).
  • Starting. crates/udpstp/src/start/ turns the options into what the runtime runs, one module per role: the Client’s Test (its TestPlan and the Client’s settings for it, ClientConfig, which the Multi-Connection Coordinator completes for each Test Connection; DF and MaxHops; socket buffers) and the Server’s ServerOptions, with the warnings to show first and the configuration errors that stop it, each with its exit status, as data; main.rs prints them and runs. The Client reads its key file and pseudonym key file there, once, and the Server its pseudonym key file. What the host allows (a DSCP, DF, the socket buffers it grants) and the Client’s name resolution come in from outside, so the rules don’t depend on the OS they are tested on.
  • OB-UDPST. udpstp reads OB-UDPST key files, and runs Tests with OB-UDPST’s Client and Server in both directions.

ADRs: 0004 (IPv4-based sizes on either IP version), 0007 (replying from the arrival address), 0009, 0010 (Key Table and Operator Keys).

2. Crates

CrateRole
udpstp-wirePDU layouts and codecs, checksum, HMAC-SHA-256 and the KDF. No protocol decisions: decoding checks only size and pduId.
udpstp-coreThe sans-IO protocol core: Client and Server roles, the Client’s Multi-Connection Coordinator, the Server’s Admission, Key Table, Sending Rate Table, Coercion, and the Data Phase of one Test Connection.
udpstp-runtimeSockets, clocks, the Tokio Control Phase loops and the Data Phase Engine. Makes no protocol decisions.
udpstpThe binary: CLI, Server configuration, key files, exit codes, output.
udpstp-key-permissionsWindows application support: borrowed-file native inspection, owned descriptor decoding and the supported private-file policy (ADR 0015). No protocol decisions.
udpstp-reqThe #[req("ID")] proc-macro attribute that ties tests to Requirements.
xtaskcargo xtask req (Requirement traceability) and known-answer test tooling.
bench-roundsThe benchmark tooling’s general machinery, which knows nothing of udpstp: compatibility helpers for the independent reference fixtures (JSON read and written as Python does, Python’s numbers, shell quoting, redaction, the rules’ statistics and times); the modules that decide: AWS’s calls, a model of EC2 with the fake-aws binary tests run, the private records of real calls and their replay against the model, the dead-man switch, cost and the ledger, a host’s counters, with the counter script, and sample readers, and the order of Round Runs; the Round bracket, which runs a Round’s body between its ledger entry and its Round Teardown, whatever happens; and the adapters that do a Round’s I/O and decide nothing: signals that only raise a flag, programs run in process groups of their own, hosts over ssh or this machine, fetched archives unpacked, the keys’ private directory, the static provider, the aws CLI run for the AWS provider, whose rented hosts it reaches over ssh, and the Round Teardown’s destroying of what a provider made. The Linux sampler produces CPU, thread and UDP records. Never published.
udpstp-benchudpstp’s benchmark Rounds on bench-rounds. The first Round’s modules that decide: its matrix, read as the Python reads it, with its Round Runs in order and the checks and figures of a Round’s plan; udpstp’s and OB-UDPST’s Client and Server command lines; each Round Run’s Maximum and Highest IP-Layer Capacity and the criterion; and the Round’s report, byte for byte the Python’s or none, read from its --out through a seam. The first Round’s body, which bench-rounds’ Round bracket runs: its keys, hosts, path check, matrix and Servers, making the Python’s calls on each host. And for either Round, the pinned OB-UDPST built on a host through bench-rounds’ Shell; and the udpstp-bench binary, whose round runs either Round type and whose report, scale-report and proxy-report write their reports. Server-scale modules own planning, judging, scheduling, host preparation and collection. Separate modules own the earlier campaign report, loopback precision suite and first-Round capture lifecycle. It reads udpstp’s JSON results and the Server’s JSON log as any consumer does, never through udpstp’s crates, so the judging stays independent of the code it judges. Never published.

The graphs highlight direct dependencies (solid) and development dependencies (dashed), grouped into the protocol, development tooling and benchmark tooling. They show the main relationships rather than every supporting library.

The protocol crates and their dependencies. The udpstp binary depends on udpstp-core, udpstp-runtime, tokio, clap, serde, toml and zeroize, and on udpstp-key-permissions with windows-sys on Windows. udpstp-runtime depends on udpstp-core, tokio and socket2, with rustix on Linux and nix on macOS; udpstp-core on udpstp-wire; udpstp-wire on hmac, sha2 and zeroize. udpstp-core and udpstp-wire use udpstp-req in their tests.
The development tooling. xtask depends on serde, sha2 and toml.
The benchmark tooling's crates. They depend on none of udpstp's protocol crates: bench-rounds depends on serde, serde_json, indexmap, tokio (for its signals), flate2, tar, getrandom and zeroize, with nix on Unix; udpstp-bench depends on bench-rounds, clap, serde, toml, indexmap and zeroize, with serde_json in its tests.

Of udpstp’s own crates, only udpstp-runtime and the binary depend on Tokio; bench-rounds uses it only to take signals. The core depends on nothing but udpstp-wire, so it builds and tests without an async runtime or a network. The workspace forbids own unsafe except for the isolated permission helper below.

Windows permission boundary: ADR 0015 isolates the native adapter in udpstp-key-permissions/src/native.rs, the sole module allowed to override its crate root’s denial of unsafe. Its safe interface borrows the file being read and returns owned data. Safe descriptor decoding and the policy live beside it and explicitly forbid unsafe, as do the protocol core, wire code, application and network runtime. The loader refuses mismatches and unverified permissions before reading content. Client errors go to stderr; Server errors also reach the configured startup log. Policy matches are quiet. The settings-only preview bypasses key loading, logs and listeners; ordinary --print-config validates keys.

ADRs: 0003 (why Tokio stays out of the core and out of the Data Phase).

3. The sans-IO core and the runtime

The core never performs I/O and never reads a clock (crates/udpstp-core/src/lib.rs). The runtime, or the test simulator, feeds it received datagrams and the time, and acts on what it returns. The boundary has two interfaces: Endpoint for the Control Phase of a whole Client or Server, and DataPhase for one Test Connection’s Data Phase.

The line between the runtime and the sans-IO core. The runtime's Control Phase loop gives the core's Endpoint each datagram, socket event and timeout with the time, and polls it for outputs: datagrams to send, sockets to open or close, a Data Phase to start, and events. For each Test Connection it takes a DataPhase from the Endpoint and moves it onto an Engine Thread, where the Data Phase Engine gives it received Load PDUs, Status PDUs, timeouts and transport errors, and asks it for Send Plans, Status PDUs and events.

Time. Every input carries a Now: a MonoTime (monotonic, any origin) and a WallTime taken together (crates/udpstp-core/src/time.rs). Timers and intervals use MonoTime. Wall-clock time is used only where the protocol carries it (authUnixTime, lpduTime, spduTime). The Data Phase builds lpduTime and spduTime from the monotonic reading plus a wall-clock offset fixed at activation, so a wall-clock step does not move them. The runtime’s Clock (crates/udpstp-runtime/src/clock.rs) is the Test Time Base: CLOCK_MONOTONIC_RAW on Linux, paired with the wall clock read between two RAW readings, and std::time::Instant elsewhere. Linux can’t sleep on RAW, so waits are durations on CLOCK_MONOTONIC (sleeps, ppoll timeouts, Tokio instants mapped from a fresh reading), and the waiter checks the time again on RAW when it wakes.

Control Phase interface (crates/udpstp-core/src/lib.rs, io.rs, event.rs):

DirectionTypeMeaning
inhandle_datagram(Now, DatagramMeta, &[u8])A datagram with its socket, source, local address, IpVersion, received TOS where the runtime read it (None on Control Phase sockets, which the runtimes don’t ask the OS for: Control PDUs carry no DSCP) and actual length.
inhandle_socket_opened / handle_socket_closedA socket the core asked for is bound, or a socket is gone. Closing releases the Test Connection’s Admission. A Data Phase’s socket goes when its Data Phase Engine exits, on every exit, and the runtime reports it after passing on the engine’s last DataEvent; the core never asks to close it.
inhandle_timeout(Now)The deadline from poll_timeout has passed.
inhandle_interrupt(Now)The Client user interrupted the Test (Ctrl-C, a signal to stop, or a closed standard output): the Multi-Connection Coordinator stops it as TestAbort::Interrupted, each Test Connection in its Data Phase sending its STOP2.
outOutput::Transmit(Transmit)Send a payload from a SocketId to an address.
outOutput::OpenSocket / Output::CloseSocketBind a Test Connection’s socket (the Ephemeral Port), or close one.
outOutput::ConnectSocketConnect a Server’s Test Connection socket to peer, the source of its first Activation Request that passed verification, before the Activation Response is sent: the OS then delivers only the Client’s datagrams and reports ICMP errors about it. A connected socket sends without an address.
outOutput::StartDataPhaseHand the Test Connection to a Data Phase Engine and accept datagrams only from peer.
outOutput::Event(Event)A typed event to log or show: admission, Silent Rejections with a SilentRejectionReason, ignored Setup Requests, Coercions, rejections, activation.

SocketId::PRIMARY is the Server’s Standard Port listeners: the IPv4 and IPv6 wildcards, or the addresses listen gives, each reply leaving from the listener its request arrived on. A listener on a specific address never sees multicast or broadcast Setup Requests. Every other SocketId is a Test Connection’s socket; the Client’s are all opened on request, SocketId(mcIndex + 1). The Client’s Multi-Connection Coordinator (crates/udpstp-core/src/coordinator.rs) also emits Output::TestConnectionEvent, a Test Connection’s event with its socket, and Output::StopDataPhase for a Data Phase it stops when it aborts a Test. Their only deadlines are the Control Phase timers (RFC 9946 §6.1, §6.2.2): the Client’s Test initiation timer ends a Control Phase that hasn’t reached the Data Phase 3 s after the Setup Request, or sooner with --time-limit (ControlPhaseFailure::TestInitiationTimedOut, which names the exchange it expired in and why the last response ignored there was, the cause the Client’s exit status and what it says follow ), and the Server closes a Test Connection with no valid Activation Request 3 s after its Setup Response (Event::ActivationWatchdogExpired), which releases it.

Data Phase interface (crates/udpstp-core/src/data_phase.rs), the one ADR 0008 froze:

  • poll_burst(Now, horizon) returns the next due SendPlan: a heap-free, Copy Send Plan of at most MAX_RUNS runs of equal-sized datagrams with contiguous sequence numbers. horizon must be zero while kernel pacing is off.
  • write_datagrams(&SendPlan, Now, buf) writes the Load PDU headers into a runtime-owned buffer just before the send, with one lpduTime for the whole Burst.
  • report_sent(&SendPlan, sent) is the only backpressure channel. The unsent datagrams become Deferred Datagrams: they lead the next Send Plan with their sequence numbers and a fresh lpduTime. When at least a Burst’s worth is still deferred, the next new Burst is a Withheld Burst (DataEvent::BurstsWithheld). Each Send Plan is reported before the next is polled.
  • handle_load_batch(RxBatch) takes header-only reads (RxDatagram: the first 32 bytes, actual length, optional kernel timestamp and TOS) with a clock pair around the read. The core converts a kernel timestamp to Test Time Base with that pair, and drops the read’s delay samples if the pair disagrees. On Linux the engine reads them in batches with each datagram’s kernel timestamp and TOS; where a platform gives no timestamp the core uses the read time.
  • handle_pdu(RxPdu) takes a Status PDU, with its kernel receive timestamp where there is one: rttRespDelay counts from it. poll_transmit(Now, buf) writes the next Status PDU when one is due.
  • handle_transport_error(TransportError) takes Icmp, MsgSize or TimeExceeded and counts them as DataEvent::IcmpErrors, PathMtuExceeded and HopLimitExceeded (ADR 0006). On Linux the engine classifies each ICMP error from the socket’s error queue; the Client’s Control Phase reads it too, and the Coordinator reports a path error there as Event::PathError.
  • stop(Now) ends the Data Phase. poll_event() returns coalesced DataEvents, ending with DataEvent::Ended(EndReason), which a Client’s DataEvent::TrialIntervalsCounted comes just before.

The simulator in crates/udpstp-core/tests/harness/sim.rs implements the runtime’s side of both interfaces deterministically from a seed, so protocol tests run without sockets. It drives the cores production does, and only those: a Server, and for each Client a Coordinator, the Client role’s one endpoint; its per-Test-Connection Client is private to it. A Test a scenario spreads over several Clients, or one the Coordinator would never make (an mcIndex used twice, two directions), is played by scripted Clients sending requests built from the RFC, since a Coordinator runs a whole Test from one host. The plan-executor contract runs the same checks against the simulator’s executor and the real Data Phase Engine on loopback: Burst spacing, ordering, partial-send reporting and the timestamp domain (testing.md).

ADRs: 0003, 0006 (transport errors as a core input), 0007 (a source address on outgoing datagrams: Transmit::source), 0008 (the frozen Data Phase interface).

4. Control Phase

The Control Phase runs on one current-thread Tokio runtime (tokio_runtime() in crates/udpstp/src/main.rs). On each side, one loop owns the core: serve() in crates/udpstp-runtime/src/server.rs and run_client() in crates/udpstp-runtime/src/client.rs. On the Server, a reader task per socket (spawn_reader in transport.rs) passes datagrams to that loop through a bounded channel. Datagrams beyond the channel’s capacity are dropped and counted.

The Control Phase through each end's runtime and core. The Client core signs a Setup Request, which the Client runtime sends to the Server's Standard Port. The Server core authenticates it or drops it silently, has the runtime bind an Ephemeral Port, and sends the Setup Response from the Standard Port and a Null Request from the Ephemeral Port. The Client verifies both and sends an Activation Request to the Ephemeral Port. The Server verifies it, checks its direction and parameters, requires an Operator Key for a Fixed-Rate Test, applies any Coercion and answers with an Activation Response. Both ends then start their Data Phase.

Step by step, with the code that handles it:

  1. Setup Request. Client::start (crates/udpstp-core/src/client.rs) picks the Key Table row valid for sending, derives the Derived Keys from the authUnixTime the request carries, and signs the request with the Client Authentication Key. Without a key file or --unauthenticated it fails with ControlPhaseFailure::NoAuthenticationConfigured.

  2. Verification and Admission. Server::handle_setup_request (crates/udpstp-core/src/server.rs) checks a Setup Request in the order the Conformance Record setup-check-order records. A request from or to a multicast or broadcast address is dropped silently, and one whose arrival address the runtime couldn’t tell is ignored, not refused (Event::SetupRequestIgnored with SetupIgnoreReason::ArrivalAddressUnknown, ADR 0007), since the reply must leave from it; the Client retries. Then the size, the checksum when checksum use is configured and the field is populated (RFC 9946 §5.6’s rule for every PDU, checksum_use::passes, with udpstp-wire’s checksum::control_valid saying whether the field matches; senders fill it last with checksum::fill_control), then authMode, the keyId’s row and its accept lifetime, then the global budget for unverified Setup Requests (dropped silently beyond it), then the digest (with Derived Keys cached per row and authUnixTime, crates/udpstp-core/src/budget.rs), then the ±5 s window. Every failure is a Silent Rejection reported as Event::SetupRequestSilentlyRejected with a SilentRejectionReason; with troubleshooting on, authentication failures are answered too, unsigned (AUTHREQ, AUTHNC, AUTHINV, AUTHFAIL, AUTHTIME). An authentic request then passes the replay cache, which drops a replay silently, and spends its Source’s budget (source_setup_burst, 48, refilled at source_setups_per_second, 5); beyond it, a signed CONNFAIL. Requests in Unauthenticated Mode, which have no HMAC, don’t spend it. Then the Key Table row’s budget (key_setup_burst and key_setups_per_second, 1000 each), which Unauthenticated Mode’s requests spend as one row; beyond it, silence, so a key holder’s flood costs at most that many admissions. Then the options: an authentic request (or one allowed in Unauthenticated Mode) with another protocolVer, or modifierBitmap flags other than CHSR_JUMBO_STATUS alone, gets a coded rejection (BADVER with the expected version, BADJS, BADTMTU), after the replay cache so a replay stays silent. Last comes Admission (crates/udpstp-core/src/admission.rs), each of whose refusals is a Setup Response, signed in an authenticated mode:

    • A duplicate of an admitted Setup Request from the same address is ignored, not refused (SetupIgnoreReason::DuplicateSetupRequest).
    • The Multi-Connection checks refuse with MCINVPAR an mcIdent of 0, an mcIndex not below mcCount or already in use in the Test, and a Test Connection that disagrees with the Test’s others on mcCount, authMode or (when authenticated) keyId; a Test is its Client IP address plus mcIdent, and never has to be complete.
    • A new Test from a Source (an IPv4 address or IPv6 /64) that already runs as many as tests_per_source allows (1 by default), then a new Test past its Source’s or Key Table row’s Quota of Tests or load bytes per Quota Period, when one is set, are refused with CONNFAIL. Only activated Tests count, so a spoofed Setup Request can’t hold a Source’s place or use its Quota; a Test’s first Activation Request checks both again, and refuses with BADPARAM a Test that Tests set up with it pushed past them.
    • A missing maxBandwidth when require_max_bandwidth is on (NOMAXBW).
    • The per-Test cap (test_connections_per_test, 24, counted over the Test’s whole life so one kept alive can’t start new measurements under it), then the places reserved by activated Tests against the global cap (max_test_connections, 256), both CONNFAIL. When the Test Connections held unactivated fill their separate share (max_unactivated_test_connections, half of the global cap but at least the per-Test cap by default; configurable above the global cap), a Setup that would otherwise be let in closes the oldest of the Key Table row holding the most of them, those of Unauthenticated Mode counting as one group, so a flooding key evicts its own (Event::UnactivatedTestConnectionEvicted).
    • The Admitted Rate Cap (admitted_rate_cap), then one Test’s share of it (max_test_share), both CAPEXC. maxBandwidth counts per Test Connection with CHSR_USDIR_BIT masked, against each direction’s cap separately, but only for activated Tests: a Test reserves its whole rate, in each direction its Test Connections state, when its first Test Connection activates, so a Setup never activated holds none, and of Tests admitted together past the cap the first to activate runs whole while the other is refused whole with BADPARAM. From then on each of its admitted Test Connections holds its rate, one still waiting for its socket included.

    On success it asks for a socket, and handle_socket_opened sends the Setup Response with the Ephemeral Port as testPort and starts the Activation Watchdog. A Setup holds its place in the unactivated share (only activated Tests reserve global places and rate) from acceptance, before its socket opens, until handle_socket_closed; a socket that can’t be opened is refused with CONNFAIL. Admission keeps every admitted Test Connection with its Test, its Source and the Quotas, and checks a Test Connection’s first valid Activation Request in this order: the direction its maxBandwidth was admitted for, then for its Test’s first the Tests per Source, the global Test Connection cap, the Admitted Rate Cap and the Quotas, which it then takes, then the Test’s one direction. Its first activation reserves global places for every admitted sibling, including one still awaiting its socket; a Test that no longer fits remains refused with BADPARAM (ActivationRejection::TestConnections) until all its Test Connections close, so closing a refused sibling cannot let the rest start. The Server keeps authentication, the budgets, the PDUs, the Activation Watchdog and the events.

  3. Null Request. Right after the Setup Response, handle_socket_opened sends a Null Request (crates/udpstp-wire/src/null.rs) from the Ephemeral Port, signed like the Setup Response in mode 1, so the Server’s firewall lets the Client in. The Client verifies it if it arrives (Event::NullRequestReceived, or NullRequestIgnored) and never answers; nothing depends on it. It leaves from the Ephemeral Port, which is bound to the address the Setup Request arrived on.

  4. Activation Request. The Client authorizes the Setup Response’s source against local Server selection before examining its contents: the selected Server endpoint, or in an authenticated mode an explicit per-Server alternate IP address, from the selected Standard Port and IPv6 scope. Client::handle_setup_response then verifies the response with the Server Authentication Key and sends the Activation Request to the signed Ephemeral Port at that authorized address. Neither a verified digest nor a Null Request authorizes another IP address. A response that fails verification is ignored (Event::SetupResponseIgnored); if it came from the selected Server’s address and rejects this Test Connection’s request, its code is still reported (Event::UnverifiedSetupRejection) as a hint when the Test initiation timer expires. A verified rejection ends the attempt with ControlPhaseFailure::SetupRejected(SetupRejectionCode), and an undefined code with UnknownSetupResponse. The runtime sends only destinations selected by the core.

  5. Activation Response. Server::handle_activation_request checks authMode and keyId against the Setup’s, then the digest and time. It rejects with CHTA_CRSP_BADPARAM an unknown direction, a second direction within one Test, an algorithm other than B or C, a Fixed-Rate Test without an Operator Key, a second activated Test from the Source, and, when an Admitted Rate Cap is set, a direction other than the one the Setup’s maxBandwidth was admitted for, or a Test whose maxBandwidth no longer fits the cap because another Test admitted with it activated first. A rejection closes the Test Connection’s socket. On acceptance it picks the Sending Rate Table row, no higher than the row matching the Setup’s stated maxBandwidth (a higher requested or Fixed-Rate row is coerced to it), sends the Activation Response, and creates the DataPhase.

  6. Coercion. Both ends compute the differences between request and response with coercions() (crates/udpstp-core/src/coercion.rs) and emit them as events. A Client configured to refuse a Coercion ends the Test with a STOP2 PDU.

Multi-Connection Coordinator. The Client runs every Test through Coordinator (crates/udpstp-core/src/coordinator.rs), the only Client-side endpoint the core exports: one Client, private to it, per Test Connection, spread round-robin across the Servers, each on its own socket. Every Setup Request goes out at once, and each Client holds its Activation Request. The Activation Requests go out together when every Setup has ended, or at the Setup deadline (1 s before the time limit, or half of it) once at least the Minimum Test Connections have succeeded; the rest are given up (Event::TestConnectionAbandoned). As soon as fewer than the minimum can succeed, the Test aborts (Event::TestAborted): before any Activation Request if it happens during the Setups. Each Data Phase starts when its own Activation Response accepts it. Each response’s parameters must match the first’s except for the Sending Rate Table row (TestParameters::same_test), or the Test aborts, and so does a Test Connection that refuses a Coercion. An abort stops the Data Phases that run, and answers an acceptance still on its way with a stop, so a Server stops its load within a round trip; one that arrives after the time limit goes unanswered, and the Server stops for want of Status PDUs. maxBandwidth is split equally, rounded down. The Load Receivers of a Downstream Multi-Connection Test keep their Status PDUs and Sub-Intervals to one grid, from when the Activation Requests went out plus the shortest Setup round trip (DataPhaseSetup::grid), so Sub-Intervals of the same number cover the same time and MeasurementResults::of_test sums them; one closed late by a stall, or that ran less than half its time, is no measurement. In an Upstream Test each Server keeps its own Sub-Intervals, so the sum by number is approximate, and the results say how far apart they were learned (MeasurementResults::approximate). The on-wire subIntSeqNo stays a per-Test-Connection count, so the Server’s log numbers Sub-Intervals its own way. Once running, a Test Connection fails when its Data Phase stops hearing its peer (DataEvent::TrafficStopped, which carries when the silence began), or ends any way but complete; the runtime and the simulator pass every Data Phase event on with Endpoint::handle_data_event. The Test goes on while at least the Minimum Test Connections haven’t failed, and otherwise stops the healthy ones; two or more falling silent within three Trial Intervals of each other are a path failure and stop the Test even when enough remain (TestAbort::TestConnectionsTimedOut). An abort mid-Test happens only while a healthy Test Connection is left to stop; those that fell silent are left to their Watchdog (ADR 0002).

A replay cache (crates/udpstp-core/src/replay.rs), shared by every socket, drops a verified mode 1 Setup or Activation Request seen again while it could still pass the time window (SilentRejectionReason::Replayed); Activation Requests are keyed on their sender too, since a Multi-Connection Test’s can be identical. Unauthenticated Mode bypasses it. In mode 2 every Status PDU is signed by its sender and verified by the Data Phase that receives it (StatusAuth in crates/udpstp-core/src/data_phase.rs).

ADRs: 0001 (KDF layout), 0009 (time window, signing, what the digest covers), 0010 (key selection, Operator Keys), 0007 (the arrival address).

5. Data Phase feedback loop

The Load Sender sends Load PDUs in Bursts from up to two Transmitters, as the Sending Rate Structure describes. The Load Receiver counts what arrives, including Sequence Errors, and sends a Status PDU every Trial Interval, starting one Trial Interval after the first Load PDU. The Server decides the rate in both directions.

The Data Phase's feedback loop. The Load Sender's Transmitters send Load PDUs to the Load Receiver, which counts them, finds Sequence Errors, keeps Sub-Interval statistics and sends a Status PDU every Trial Interval. On the Server, interval classification feeds algorithm B or C, which picks the next Sending Rate Table row: the Server's own Load Sender uses it in a Downstream Test, and the Server's Status PDUs carry it to the Client in an Upstream Test. In an Upstream Test the Server also polices the rate it receives.

What exists today (crates/udpstp-core/src/data_phase.rs):

  • The row. The Server takes the row from the Activation Request: row 0 for CHTA_SRIDX_DEF, otherwise srIndexConf capped at the row the Client’s maxBandwidth allows, and, for a search without an Operator Key, at max_start_row (Server::requested_row); a search then climbs from there up to the maxBandwidth row. The Sending Rate Table (sending_rate_table.rs) has OB-UDPST’s 1153 rows for the Server’s CHSR_JUMBO_STATUS and CHSR_TRADITIONAL_MTU settings, which a Setup Request must match: row N is N Mbps up to row 1000, 1250-byte packets or 1500 with Traditional MTU; above it OB-UDPST’s rates, Jumbo rows fitted to the Server MTU (server_mtu, 1500 by default) with more, smaller packets, each row never above its rate. The Server goes no higher than the first row at the table’s top rate, so a search can’t climb into repeated rows. At high rates the portable engine may fall behind well before the table’s top, sending a packet a call and receiving one a call on Windows and macOS; on Linux, which sends and reads in batches, a Load Sender’s thread topped out at about 4–5.6 Gbps over loopback in WSL2 (Skipped Bursts and loss report it; linux-batched-sending.md). The Load Rate Adjustment Algorithm moves the row after each Trial Interval, except in a Fixed-Rate Test.
  • Downstream. The Server is the Load Sender and sends from its row. The Client’s Status PDUs carry a zero Sending Rate Structure.
  • Upstream. The Server is the Load Receiver, and each of its Status PDUs carries its row’s Sending Rate Structure. The Client applies it with LoadSender::set_rate after sanitising it to OB-UDPST’s field limits.
  • Catch-up by time budget. A missed Burst less than the catch-up budget late (1 ms, 2 ms on Windows, set by the runtime; ADR 0012) is still sent, oldest first, one Send Plan at a time; older ones are Skipped Bursts, reported as DataEvent::BurstsSkipped. The most recent missed Burst always goes (ADR 0008, ADR 0012).
  • Over-rate policing. As the Load Receiver, the Server stops the Test Connection after 3 Trial Intervals in a row above 1.5 times the highest rate it assigned within the last second (EndReason::OverRate; ADR 0011).
  • Stale Status PDUs. A Status PDU whose spduSeqNo isn’t greater than the last one processed only counts as heard: it can’t change the rate, deliver STOP or be echoed for RTT, and is reported as DataEvent::StaleStatusPdus. Load PDUs carry spduSeqErr, the Status PDUs missing before the latest one, as OB-UDPST counts it.
  • IPv6. A Load Sender on IPv6 takes 20 bytes off every size in the Sending Rate Structure.
  • Delay variation and RTT. The Load Receiver takes each in-order Load PDU’s receive time minus its lpduTime, in ms rounded as OB-UDPST rounds, and reports it less the running minimum (clockDeltaMin). The Load Sender echoes the latest spduTime with rttRespDelay; the Load Receiver turns that into rttMinimum and rttVarSample. Both run on the Test Time Base, so a wall-clock step or slew during a Test changes neither.
  • Interval classification. The Server classifies each Trial Interval as a Clean, Errored or Hold Interval, from the Status PDU it receives (Downstream) or sends (Upstream), and reports it as DataEvent::TrialIntervalClassified (crates/udpstp-core/src/interval.rs). DelayVar is the maximum one-way delay variation, the delay range the RFCs judge (the average if the Server’s delay_var_statistic setting says so, as OB-UDPST does), or the latest RTT sample when useOwDelVar is off. An interval with no delay sample is never clean. The Client classifies them too, the Status PDUs it sends (Downstream) or receives (Upstream), always by the maximum, only to count them (IntervalCounter, counters only): as its Data Phase ends, DataEvent::TrialIntervalsCounted says how many were Clean, Errored and Hold Intervals, how many of the Hold Intervals had no delay sample, and whether slowAdjThresh Errored Intervals ever came back to back, confirming Congestion. It can’t see the Server’s Lost Status Backoffs, and an OB-UDPST Server classifies by the average by default, so the two counts can differ.
  • Algorithm B. Each classified Trial Interval moves the Server’s Sending Rate Table row (crates/udpstp-core/src/algorithm.rs), and the row it chose rides on the classification event. In Fast Mode a Clean Interval adds highSpeedDelta rows, up to the 1 Gbps row. slowAdjThresh Errored Intervals back to back latch Congestion Confirmed and take 3 × highSpeedDelta rows off once (ADR 0005); every other change is one row. Downstream the Server sends at the new row at once; Upstream the row goes out in the Status PDU that reports the Trial Interval. A Fixed-Rate Test holds its row.
  • Algorithm C. In Fast Mode it doubles the row on every second Clean Interval (row 0 goes to row 1 first), jumping to the 1 Gbps row when doubling would pass it. Its single steps count toward RetryThresh (5 at first); at it, a Fast-Mode Retry clears Congestion Confirmed, and after a retry from a step up RetryThresh grows by 5. The Client asks for it with --algorithm c.
  • Lost Status Backoff. In a Downstream Search Test, when no valid Status PDU has arrived for upperThresh + (2 + w) × trialInt since the last one (or since activation), the Server backs off as for an Errored Interval, which can confirm Congestion, and w counts up. w starts at the Setup Response’s round trip divided by trialInt, rounded up and capped below the 1 s Status PDU timeout, so a long path gets no backoff before its first Status PDU; any valid Status PDU, stale or not, starts it over (RFC 9097 §8.1, ADR 0005 as amended). A backoff is reported as DataEvent::LostStatusBackoff. Upstream Tests rely on the 1 s timeout.

The core computes a Test’s measurement results from the Data Phase’s events (MeasurementResults in crates/udpstp-core/src/results.rs): one Test Connection’s, or a Multi-Connection Test’s summed, with each Sub-Interval, the IP-Layer Sender Bit Rate over it, measured by the Load Sender or else estimated by the Load Receiver, and each Capacity Mode’s Maximum and Highest IP-Layer Capacity, under the Client user’s Evaluation (Performance Criterion and Capacity Mode split). When the Client learned each Sub-Interval is a runtime fact, which the binary keeps. It also judges a Search Test’s searches (SearchTest in crates/udpstp-core/src/search.rs): whether one found the path’s limit, by its Client’s count of its Trial Intervals. Below the 1 Gbps row a search that rises past the limit confirms Congestion at its first overshoot. Above it the search steps a row at a time, and through a shallow queue its Errored Intervals can come one or two at a time, too few back to back to confirm Congestion: it met the limit when, of its last 64 Trial Intervals, from the first Errored Interval among them on, at least 4 and at least 1 in 4 were errored, Hold Intervals of both kinds counted among them, which the count keeps in a 64-bit register. When no Test Connection’s search found the limit either way, it says why each fell short, the first that fits: its IP-Layer Sender Bit Rate stopped at the maxBandwidth it stated, at the highest row at or below it, or reached the top of the Sending Rate Table, judged from the size of the Load PDUs sent at its peak, which the Server’s MTU decides; more than half its Trial Intervals were Hold Intervals with a delay sample; it was still rising at the end by more than Fast Mode adds with half its Trial Intervals clean; or else it stopped rising though few of its last Trial Intervals were errored, so the Load Sender may not have kept up. Its known limits are in the running-a-test guide: the Client’s count can differ from the Server’s (the Trial Intervals after STOP1, Status PDUs lost on the way back, the Server’s Lost Status Backoffs, algorithm C’s Fast-Mode Retries, and an OB-UDPST Server’s looser count and average delay variation), and a Load Sender held to a packet rate steps up as its Load PDUs grow past the 1 Gbps row, which a short Test reads as still rising. By the same 5% margin it judges whether a Downstream Fixed-Rate Test’s Server kept its rows’ sending rate where each Maximum IP-Layer Capacity was measured, as the Client estimated what it sent (FixedRateTest); the binary asks only once every activated Test Connection completed. The binary builds a Test’s results once, from the runtime’s TestOutcome and the options that shape them, in TestResults (crates/udpstp/src/test_results.rs): the whole Test’s results and each Test Connection’s, how each ended, the warnings, what is said as the Test ends, and the exit status. How one Test Connection ended is one module’s answer (TestConnectionEnd in crates/udpstp/src/test_connection_end.rs): from its outcome, its ErrorStatus and whether it completed, its names in the JSON results, how its Data Phase ended, and what is said of it, the same for a Test Connection of a Multi-Connection Test, named by its mcIndex, which says once what concerns the whole Test. Its exit status and its words both follow one private account of why it ended, each by an exhaustive match with no catch-all, so a new way to end can’t get one without the other; exit.rs only combines a Test’s statuses as OB-UDPST does. The text and JSON results (crates/udpstp/src/results.rs, json.rs) only render it.

ADRs: 0005 (Congestion Confirmed latch), 0002 (silence timeouts), 0004, 0008 (bounded catch-up, over-rate policing), 0011 (policing against the rates of the last second), 0012 (catch-up by time budget).

6. Data Phase Engine

Tokio’s timer is 1 ms-granular and was measured more than 1 ms late, so nothing precise runs on it (ADR 0003). The Control Phase hands each activated Test Connection to a Data Phase Engine, which sends its Send Plans at their scheduled times.

The runtime's threads. A Tokio current-thread runtime runs the Control Phase loop, which owns the Endpoint, and a reader task per socket, which feeds the loop through a bounded channel and stops when the Data Phase starts. For each Test Connection the loop spawns an Engine Thread that owns its DataPhase, a clone of its socket and the clock; the engine reports its events and its exit over a channel, and the loop stops it with a stop flag. On Linux each engine sends its Load PDUs in batches with sendmmsg and UDP GSO, and reads in batches with recvmmsg and kernel receive timestamps, waiting between turns in ppoll on its socket. A Linux Server instead hands each Test Connection to one of its Engine Threads, at most one per CPU the process may use, each serving many Test Connections from one loop over an epoll set, a deadline heap and a ready list; stops go to it as commands, with an eventfd to wake it.

One thread per Test Connection: the per-Test-Connection executor. crates/udpstp-runtime/src/engine.rs runs one OS thread per Test Connection: the Client’s on every OS, and the Server’s on Windows and macOS (a Linux Server runs the pooled executor, below; ADR 0008). The Test Connection’s socket is cloned when it is bound (bind_shared). When the Data Phase starts, the Tokio reader task is aborted and the Engine Thread reads the clone, non-blocking, through the socket module (below). The Control Phase then lets go of its Tokio handle, so the socket leaves Tokio’s reactor, whose registration would otherwise wake the Control Phase’s thread for every datagram that arrives and every send the kernel completes; the Client keeps the socket open, out of the reactor and unread, until the Test ends, so the peer’s last datagrams draw no ICMP errors. The Test Connection’s protocol work is its slot’s (Slot in engine/slot.rs: its DataPhase, its socket, and a method for each step of a turn below), and the thread is the per-Test-Connection executor, which owns one slot, runs its turns and waits between them, so another executor can drive the same slots. The executor also says when the slot reads the clock (engine/times.rs): this one reads a fresh clock pair (three clock_gettime calls on Linux) for every core call, and before and after every read of the socket, as it always has; Turn, for an executor that serves many slots a turn, reads one pair as the turn starts, the Test Time Base alone (Clock::mono, one call) wherever the core judges or stamps time, with the wall clock carried on from the turn’s pair, and a pair after each read that returned datagrams, none around a read that returned nothing. Each turn (PerTestConnection::run):

  1. If the stop flag is set, call DataPhase::stop once.
  2. Send every due Send Plan whole: poll_burst, write_datagrams just before the send, the datagrams (Socket::send_plan), then report_sent with how many reached the kernel. A send the socket can’t take ends the Send Plan there, the rest becoming Deferred Datagrams. The turn sends for at most 1 ms (SLICE), and between the Send Plans of a catch-up it reads what arrived (ADR 0012).
  3. Read what has arrived (Socket::receive), so a Status PDU waiting in the socket counts before any timeout is judged.
  4. Handle a due timeout, then send every due Status PDU (poll_transmit, Socket::send).
  5. Forward DataEvents to the Control Phase. After DataEvent::Ended, exit.
  6. Wait until the next deadline (next_burst_at or poll_timeout): the wait policy (wait.rs). On Linux the engine waits on its socket (Socket::ready, ppoll), at nanosecond resolution and at most 10 ms (IDLE_CAP), so an arrival ends the wait at once. After a turn that read datagrams, a Load Receiver leaves POLLIN out for a receive hold-off, a quarter of the time its receive buffer holds at the arrival rate, within 100–250 µs, so it reads what arrives meanwhile with one call rather than waking for each datagram. It holds off only when more than one datagram is due within the hold-off, at the rate datagrams arrived in its last window of at least 10 ms, since a hold-off that ends with nothing to read costs a wake-up of its own: at row 4, a Load PDU every 1 ms, it waits for each arrival and wakes once per Load PDU, where it woke twice. The rate is what arrives, not the row, which the Client doesn’t know in a Downstream Test; until a window with arrivals in it has closed, at the start and after a pause, a Load Receiver holds off as before. Rows that send a Burst of five to ten Load PDUs every 1 ms (41 to 99) still hold off, and wake twice a Burst as before where the Burst arrives back to back. Waking on each arrival leaves the wake-up to the receive softirq, which then more often wakes a CPU that halted: in a VM that costs it more per Load PDU (research/receive-hold-off.md has the measurements, and what decides whether the rule stays). A Load Sender takes each Status PDU at once, holding off only when datagrams arrive less than 1 ms apart, a flood. A pending error (POLLERR) is drained from the error queue in the wait, by the socket, whose wait won’t leave one pending (Socket::ready). A spin guard counts wake-ups that made no progress and, after 8 in a row, sleeps through the next 64 waits, at most 250 µs each. Elsewhere, and where the kernel refuses ppoll (ENOSYS, EPERM, EINVAL), the engine sleeps, at most 1 ms (SLICE), so an arrival waits at most that long. Each Engine Thread on Linux runs with 1 ns timer slack (ADR 0008).

A Test Connection’s socket (crates/udpstp-runtime/src/socket/) is the per-socket I/O behind one interface, Socket: send a Send Plan’s datagrams, or one more datagram, and say how many reached the kernel; hand what arrived from the peer to a sink (Sink: Status PDUs, Load PDUs with the clock pair read around each read, and path errors); wait until ready, draining a pending error to the sink (Linux); and report the fast paths it used (FastPaths), its Receive Buffer Overflows and its send lag. Behind it:

  • Sending goes with the fastest calls the kernel takes. On Linux in batches (socket/batch.rs): sendmmsg calls of up to 64 messages, each run of equal-sized Load PDUs one UDP GSO message, ending with a smaller Add-on where there is one. A GSO send the kernel or device refuses (EIO, EINVAL, EMSGSIZE, EOPNOTSUPP, ENOPROTOOPT) turns GSO off for the Test Connection, and a refused sendmmsg (ENOSYS, EINVAL, EIO, EOPNOTSUPP) turns batching off; then, and elsewhere, one send per datagram. Before the engine starts, the socket is connected to the peer on Linux, on the Client too, so its sendmmsg calls carry no address; the Server’s is already connected.
  • Reading is bounded per turn: up to 256 datagrams in batches on Linux (READ_BUDGET), and elsewhere one at a time for at most 1 ms (READ_SLICE; a count of 256 left a Windows Load Receiver behind at 2.1 Gbps). On Linux they are read in batches (socket/batch_read.rs): recvmmsg calls of up to 64 datagrams, a Load Receiver’s header-only with MSG_TRUNC, each with its SO_TIMESTAMPNS receive timestamp and TOS, and one clock pair read around each call. The socket asks for receive timestamps and the TOS (IP_RECVTOS, IPV6_RECVTCLASS) and turns off the arrival addresses the Client’s Control Phase asked for, so every datagram carries the same control messages, which nix’s recvmmsg needs (the count of Receive Buffer Overflows, where the socket asks for it, arrives only from the first overflow on, so the first read after it loses its control messages, once as a rule, and the count is unknown until a datagram’s arrive whole again); a refused recvmmsg (ENOSYS, EPERM, EINVAL, EOPNOTSUPP) turns batched reading off, for one recv_from per datagram, timed by the read. Datagrams not from the peer are dropped; Status PDUs go to the sink one by one, and the Load PDUs between them together.
  • Transport errors (ADR 0006) are classified once, by one classifier per OS (socket/classify.rs), given the call that failed (a send, a sendmmsg with or without GSO, a send whose messages carry the numbers of their kernel send timestamps, a read, a recvmmsg, a ppoll, a Control Phase send or read) and the error: make the call again; a full send buffer, so the rest waits; a path error of a kind to report, a send being made once more; a datagram too large for the Path MTU, which counts as sent and lost; a fast path the kernel refuses, which the socket stops using; or fatal, the only failure a caller sees. The same code can mean different things on different calls: EINVAL ends a plain send or read, but refuses the fast path on the batched calls and ppoll, and on a send whose messages carry their numbers may be the kernel refusing the numbers; EMSGSIZE refuses GSO on a GSO send, and is a Path MTU event on any other. Every error number an OS returns that the runtime acts on is named there and nowhere else, each OS’s in a table test, and so is what an entry of Linux’s error queue means (socket/errqueue.rs).
  • Kernel send timestamps. With UDPSTP_KERNEL_TX_TIMESTAMPS=1, an experimental setting, a Linux Load Sender asks for software send timestamps (SO_TIMESTAMPING with OPT_ID and OPT_TSONLY), drains them from the error queue with the ICMP errors, matches each to its Send Plan by the number it comes back with, and reports how much later than lpduTime the kernel sent it (socket/send_stamps.rs). Where the kernel takes it (Linux 6.13 and later, found by a probe once a process), each message carries its own number in an SCM_TS_OPT_ID control message, so a send the kernel refuses shifts no later number, and a Test with refused sends keeps its send lag. The numbers start at 2^31, half the u32 range away from the kernel’s own, which start at 0: a kernel that ignored them would stamp the messages with numbers that match no Send Plan, giving no lags rather than wrong ones. nix’s sendmmsg shares one set of control messages across a call, so the Linux adapter then sends each message of a batch with a sendmsg call of its own (socket2’s) and reports how many the kernel took, as sendmmsg would; it offers batches that don’t go in one sendmmsg call, so the socket reports UDP GSO without sendmmsg among the fast paths. A numbered send the kernel refuses with EINVAL, as it refuses a number it doesn’t take (the classifier’s reading: socket/classify.rs), goes again without the numbers; if that goes, messages carry none from then on, batches go in sendmmsg calls again, which the socket then reports, and no more lags are counted, so the experimental measurement can’t fail the Test. Elsewhere the kernel numbers the messages, and it can number one it then refuses, so the count stops at the first send refused, whole or in part. UDPSTP_KERNEL_TX_TIMESTAMPS=kernel-count leaves the numbering to the kernel on every kernel (the socket’s Settings::explicit_numbers off), so a result can be checked on that send path, with batches in sendmmsg calls. Matching each timestamp by the Load PDU the kernel can queue with it was measured and not chosen: over loopback each copy takes up to 46 KB of the receive buffer the Status PDUs share (research/send-stamps.md).
  • Adapters. Portable sends and reads one datagram per call with std’s socket (Windows, macOS). Linux adds the batched calls, ppoll, the error queue, send timestamps and Receive Buffer Overflows (socket/overflows.rs), and makes its single sends and reads with Portable’s calls. It counts the overflows from the process’s own UDP tables in /proc/self/net when the Data Phase ends, or, where it can’t read them (since Linux 7.2, a /proc mounted with subset=pid by a mounter without CAP_NET_ADMIN over the network namespace hides them), from the count the kernel attaches to each datagram read in batches (SO_RXQ_OVFL), which misses the overflows after the last datagram read. An Open recipe given to the engine at spawn (engine::spawn_with) opens the adapter on the Engine Thread, since nix’s header sets aren’t Send: the platform’s own (Platform), or, in tests, the fault adapter (socket/faults.rs), which wraps either and injects faults into its calls. Everything is dispatched statically.
  • Engine settings. The experimental variables that change how the engine runs (UDPSTP_ENGINE_CATCH_UP_US, UDPSTP_ENGINE_HOLD_OFF_US, UDPSTP_KERNEL_TX_TIMESTAMPS, and a Linux Server’s UDPSTP_ENGINE_THREADS and UDPSTP_ENGINE_GROUPING_US) are read by the binary once, in start::engine_settings (crates/udpstp/src/start/mod.rs), and passed as one value, EngineSettings, in ServerOptions and to run_test, which give it to each engine at spawn. The runtime reads none of them; only the test-only UDPSTP_FAULTS, which belongs to its fault-injection feature.

The turn loop and the wait policy stay outside the socket module, so the pooled executor (below) drives the same sockets with a loop and a wait of its own. When the engine exits, however it ends, FromEngine::Exited carries its account of how the Data Phase ran on this host as one value, EngineFacts (crates/udpstp-runtime/src/engine.rs): the socket buffers it got as it started, the fast paths it used, its socket’s Receive Buffer Overflows and, when asked for, the kernel’s send lag. The Control Phase passes it on unread, in ClientOutcome::engine_facts and the Server’s ServerNotice::DataPhaseEnded, so a fact the engine adds reaches the Client’s results and the Server’s log without touching the loops; the binary’s renderers read the facts they show, and the Client’s results combine a Multi-Connection Test’s in one place (crates/udpstp/src/test_results.rs).

A panic on the engine’s thread, as its socket opens or in the turn loop, is caught (catch_unwind). It ends only that Test Connection, and the engine always reports FromEngine::Exited. The Control Phase then closes the socket and calls handle_socket_closed, which releases Admission (ADR 0003); tests that inject the panic, mid-Test and as the socket opens, prove it (the fault adapter). The engine reports through an unbounded channel, so a busy Control Phase never blocks it. Dropping or stopping the Engine handle sets the stop flag.

The Linux Server: the pooled executor. A Linux Server runs its Test Connections’ slots on Engine Threads (engine/pool.rs, engine/pool/thread.rs), its only executor; the Client and every endpoint on Windows and macOS keep a thread per Test Connection (ADR 0008). The pool starts at most as many Engine Threads as the CPUs the process may use (available_parallelism: its affinity mask within a cgroup quota; UDPSTP_ENGINE_THREADS overrides it), lazily, unpinned, each named udpstp-eng-<index>, and places a new Test Connection on the one serving the fewest, never moving it (engine/schedule.rs, place). The Server logs how its engines run once at start (DataPhaseEngines). EnginePool::start makes the socket ready on the Control Phase’s thread and sends the Test Connection to its Engine Thread over a channel, writing its eventfd; stops go the same way, so a waiting Engine Thread sees one at once. Each Engine Thread’s turn: one clock pair (Turn); the commands; readiness harvested from its edge-triggered epoll set with a zero timeout onto a ready list (a socket with an error pending marked so, drained before it is read); every due Burst, earliest deadline first from a deadline heap, for at most 1 ms in all, the rest in a turn that follows at once; each slot on the ready list read up to its 256-datagram budget, one whose budget ran out going to the back for the next turn; the due timeouts and Status PDUs of the slots it touched, a Load Receiver whose timeout is due read first; their events; then the heap re-keyed. It waits in ppoll on its epoll fd and eventfd until the earliest deadline, at nanosecond resolution, or with none until an arrival or a command. receive hold-off is the Engine Thread’s: after a turn that harvested and read, when more than one datagram is due within it at the rate all its sockets received at, the epoll fd is left out of the wait until a fixed end, at most 250 µs and at most the shortest share of a reading slot’s receive buffer less its longest recent turn, none under 100 µs. A known gap: the buffer share counts the payload bytes read, not the memory the kernel charges for them, so on loopback and veth, which charge a datagram split from a UDP GSO message far more than its size, a fast Load Receiver sharing an Engine Thread with Load Senders can overflow its receive buffer; over a NIC the share holds. A later fix would judge the share by the socket’s receive memory (engine/schedule.rs, HoldOff, has the details). With a grouping window (UDPSTP_ENGINE_GROUPING_US, an experimental setting, 0 by default, since no measurement has yet shown a window worth making the default: ADR 0008), a wait ends at the last deadline within the window of the earliest, never past a hold-off’s end. The spin guard and the sleeping fallback where the kernel refuses ppoll are the Engine Thread’s. Each slot’s work runs in a catch_unwind of its own, so a panic retires that Test Connection alone, out of the epoll set before its socket closes; every handover carries an exit reporter whose Drop reports its end unless it was reported, so a panic in the Engine Thread’s own loop, or a handover queued to one that died, still reports every end and so releases every Admission; a handover to a dead Engine Thread goes to another. Where the Receive Buffer Overflows are read from the process’s UDP tables, an ending Test Connection’s socket goes to one thread of the pool’s, which reads the tables once for each batch of ends and reports each end, so an Engine Thread never parses /proc. Its sockets report the fast path epoll, not ppoll. Its policy is pure and tested alone (schedule.rs); on loopback it meets the plan-executor contract with 1 and with 32 Test Connections on one Engine Thread (engine/pool_tests.rs), and a test run in a process of its own checks that 8 Tests on one Engine Thread allocate nothing in a second of their steady state (ADR 0008), with a counting global allocator in the test build (stats_alloc).

ADRs: 0003, 0008, 0006.

7. Test Connection lifecycle

In the Control Phase, the Client holds its state in the State enum (crates/udpstp-core/src/client.rs). The Server tracks a Test Connection in its Admission (crates/udpstp-core/src/admission.rs): admitted, it waits for its socket; once the socket is bound it is set up, and later activated, the Server’s own data about it (its Setup Request, its authentication, its Activation Watchdog) kept alongside. In the Data Phase, both ends move through the Phase enum in crates/udpstp-core/src/data_phase.rs, each knowing whether it is the Test Connection’s Client or its Server (End): in the STOP exchange the Server stops and the Client confirms.

The states of a Test Connection. It starts in the Control Phase when a Setup Request is verified, and is admitted once its Ephemeral Port is bound and the Setup Response sent. An Activation Response with ACKOK starts the Data Phase; BADPARAM, or no Ephemeral Port to bind, releases it. In the Data Phase both ends start Testing. The Server moves to Stopping after the Test Interval plus 500 ms or on over-rate, and ends when the Client confirms the STOP exchange or a Trial Interval plus 1 s passes. The Client moves to Confirming when it receives the Server's STOP and ends after its confirmation or its Test Interval deadline. A local stop at either end also uses Confirming, but sends STOP2 immediately and ends within 1 s; an end already silenced by its receive timeout ends without sending. An early STOP received by the Server or an expired Client timer ends Testing at once. When its engine exits and its socket closes, the Test Connection is released.
  • Ends of the Data Phase. DataEvent::Ended carries an EndReason: Completed, Unconfirmed, TestIntervalElapsed, StoppedByClient, Stopped, Watchdog or OverRate. EndReason::completes says which of them complete the Test Connection: Completed, and TestIntervalElapsed, which OB-UDPST counts as complete too; the Multi-Connection Coordinator and the Client’s exit status and results all ask it. After 1 s without valid PDUs the endpoint stops sending (DataEvent::TrafficStopped), and the Watchdog ends the Test Connection 3 s after the last PDU. The Client’s Test Interval timer runs to the Test Interval plus 1.5 s, which leaves room for the Server’s STOP2 to arrive.
  • Released. On the Server, handle_socket_closed removes the Test Connection, releases its Admission and emits Event::TestConnectionReleased. The Server’s runtime calls it when the engine exits, however it exits, after passing on the engine’s last Data Event, so what ended the Data Phase is reported before the release; when it closes a socket the core asked it to (a BADPARAM rejection, an Activation Watchdog, an eviction, a shutdown); when no Ephemeral Port could be bound; when a Test Connection’s socket reader fails before its Data Phase; and when the core asks it to stop a Data Phase that isn’t running. The simulator does the same. The Client’s runtime reports every engine exit the same way, and the Multi-Connection Coordinator counts one that exited without DataEvent::Ended as a failed Test Connection. On the Client, run_test returns once every Test Connection’s engine has exited and the Multi-Connection Coordinator has no deadline left. The Client ends in its State::Ended on a rejection or a Control Phase failure. When the Server administrator stops the Server, Server::handle_shutdown stops each Test Connection in its Data Phase (Output::StopDataPhase) and closes any other, and each is released as it goes.
  • A Test Connection abandoned before activation is closed by its Activation Watchdog 3 s after the Setup Response, which releases its socket and Admission.

ADRs: 0002 (traffic stops at 1 s, the Watchdog ends the Test Connection at 3 s), 0003 (Admission released on every exit path), 0008 (over-rate stop).

Where to start reading the code

  1. crates/udpstp-core/src/lib.rs: the Endpoint trait and the public API.
  2. crates/udpstp-core/src/io.rs, time.rs, event.rs: what crosses the boundary.
  3. crates/udpstp-core/src/server.rs and client.rs: the Setup and Activation exchanges.
  4. crates/udpstp-core/src/data_phase.rs: DataPhase, SendPlan, LoadSender, LoadReceiver and the STOP exchange.
  5. crates/udpstp-runtime/src/server.rs and client.rs: the Tokio Control Phase loops.
  6. crates/udpstp-runtime/src/engine.rs: the portable Data Phase Engine; socket/mod.rs: a Test Connection’s socket, which it drives.
  7. crates/udpstp-wire/src/lib.rs, then setup.rs, activation.rs, load.rs, status.rs, auth.rs: PDU codecs and authentication.
  8. crates/udpstp/src/main.rs, config.rs, start/, keyfile.rs, service.rs: the binary, configuration, what each role starts with, key files, and the Server as a Windows service (ADR 0014).
  9. crates/udpstp-core/tests/harness/: the deterministic simulator the protocol tests run on.
  10. crates/udpstp/tests/loopback.rs: end-to-end Tests of the real binary over loopback.

Technology

udpstp uses Rust for the protocol, runtime and command-line application. The architecture guide maps the crates. GLOSSARY.md defines their domain terms.

Language and toolchain

The workspace uses Rust edition 2024 and Cargo resolver 3. Cargo.toml records the minimum supported Rust version. rust-toolchain.toml selects stable Rust and the development components. The project supports its documented range of stable Rust releases, with the oldest supported version tested in CI.

First-party Rust is safe except for the approved, isolated Windows Shared Key permission adapter in ADR 0015. Its safe decoder and policy retain the unsafe-code prohibition, as do the protocol, wire code, network runtime and ordinary application code. Expanding the exception requires the maintainer’s explicit approval. Dependencies may contain unsafe code internally.

Formatting uses rustfmt. CI denies Clippy warnings across the workspace and tests. The release profile retains unwinding so the Data Phase Engine can contain a Test Connection’s panic, as required by ADR 0008.

The Windows binary links its C runtime statically through .cargo/config.toml. Checks guard against accidentally adding a dependency on the Visual C++ Redistributable.

Libraries and boundaries

AreaLibraries and purpose
Wire format and authenticationRustCrypto hmac and sha2, with independent known-answer vectors. zeroize wipes key material. ADR 0001 fixes the KDF layout.
Protocol coreDeterministic state machines with no I/O. Time, received PDUs and transport outcomes arrive as inputs.
Control PhaseTokio drives sockets and second-scale deadlines. Precise Data Phase pacing stays outside Tokio timers under ADR 0003.
Data PhaseDedicated engine execution, Send Plans and bounded catch-up under ADRs 0008, 0011 and 0012.
Socket interfacessocket2, nix and rustix provide supported operating-system APIs. quinn-udp is restricted to the Windows Standard Port listener under ADR 0007.
Applicationclap handles arguments and help. Serde and TOML handle configuration and structured data. Tracing supplies logs.
Windows servicewindows-service integrates with the Service Control Manager under ADR 0014.

Exact dependency versions and features live in the Cargo manifests and lockfiles. Platform differences describes the operating-system limits and available fast paths.

Dependency audit

cargo deny check checks advisories, licenses and dependency sources under deny.toml. The Dependency audit job runs in the full CI profile.

Build and CI

CI runs for pull requests, changes to main and manual dispatch. The full profile covers native Linux, macOS and Windows checks. Documentation-only changes use the documentation profile.

The maintained commands check formatting, Clippy, workspace tests, RFC traceability, links and glossary terms. Linux integration checks cover network namespaces, OB-UDPST, systemd, containers and packaged crate verification. Windows checks cover the installer and Server service. The website has its own build and metadata checks.

See the validation workflow for commands and tier boundaries.

Nightly is manually dispatched. It runs checks kept outside the normal gate, including fuzzing, mutation testing, quarantined tests and precision diagnostics.

xtask

cargo xtask is the alias in .cargo/config.toml.

CommandPurpose
cargo xtask reqValidate RFC Requirement IDs, classifications and proving tests, and update conformance output.
cargo xtask linksCheck relative documentation links and anchors.
cargo xtask glossaryCheck domain terms and documented exceptions.
cargo xtask site prepareStage the documentation, generated reference and website sources.
cargo xtask site finishFinish the site and check its links, metadata and presentation requirements.

Release packaging

The release artifact guide describes archives, native packages, checksums, dependency inventories and attestations.

Container and package-repository workflows reuse the released binaries. The installation guide records distribution availability.

Decisions

The Architecture Decision Records: decisions that are hard to reverse, why they were made, and the status of each.

KDF fixed-input layout follows RFC 9946 Appendix A

RFC 9946 §5.4.1 names the KDF inputs (counter-mode NIST SP 800-108, HMAC-SHA-256, r = 32, Label “UDPSTP”, Context = decimal authUnixTime, 512 output bits) but leaves the byte layout open: at least five readings are consistent with the prose, and each derives different keys. We take the layout that Appendix A’s OpenSSL KBKDF call produces, BE32(i) ‖ "UDPSTP" ‖ 0x00 ‖ decimal(authUnixTime) ‖ BE32(512), because it is the only one the RFC pins down in code, OB-UDPST v9.1.0 uses it, and four independent computations (OpenSSL 3.5 on Windows and WSL, a hand-written HMAC construction, and the RustCrypto kbkdf crate) agree on its known-answer vectors.

Consequences

  • A reader following only the §5.4.1 prose might expect no separator byte or no length field; do not “fix” the layout to match the prose.
  • The Shared Key is fed to the KDF as raw bytes. Unlike Appendix A’s strlen(Kin), it is not cut at the first zero byte; for text keys (as OB-UDPST uses) the two agree.

Stop traffic after 1 s of silence, tear down the Test Connection at 3 s

RFC 9946 contradicts itself on timeouts: §5.1 items 7–9 require the Load PDU and Status PDU timeouts to stop the test within at most 1 s of silence, while §6.1 describes a 1 s watchdog that only warns and a termination 2 s later (which OB-UDPST and TR-471 implement as a 3 s stop). We satisfy both: after 1 s without Load PDUs the Load Receiver stops sending Status PDUs, and after 1 s without Status PDUs the Load Sender stops sending Load PDUs (§5.1), which bounds any self-induced congestion; the Test Connection itself, with its socket and state, is torn down at 3 s (§6.1).

Consequences

  • Against OB-UDPST, our endpoints go quiet earlier than OB-UDPST does (1 s vs 3 s). Tear-down times differ by up to about 1 s (see below).
  • Traffic that resumes between 1 s and 3 s does not restart load, nor reset the Watchdog; the Test Connection is already ending. §5.1’s “SHALL be closed” is read as final, over §8.1’s “until traffic is received again”.
  • rxStopped is asserted from 0.5 s, half the 1 s timeout, so the PDUs sent before an end stops carry it. §8.1 recommends 1 s, which the 1 s timeouts would never let anyone see.
  • An end that stopped sending also stops refreshing its peer’s Watchdog. After a break in one direction, an OB-UDPST peer therefore tears down about 1 s after we do (its Watchdog was reset by our last PDU, about 1 s later than ours), and it reports traffic stopped in both directions. This is an OB-UDPST Difference.

Tokio runs the Control Phase; nothing precise runs on Tokio timers

The Control Phase (listening on the Standard Port, Setup and Activation exchanges, idle ephemeral sockets, second-scale watchdogs) is many mostly-idle I/O tasks, which is what an async runtime is good at, so it runs on Tokio. The Data Phase is the opposite: a few Test Connections each moving up to about 1 Mpps with 100 µs send intervals, where the rate comes from batched I/O (sendmmsg/GSO, recvmmsg) and precision from the timer. Tokio’s timer is 1 ms-granular by design and measured more than 1 ms late, so no pacing, Status PDU timer or other precise deadline ever runs on Tokio timers.

The Data Phase runs in a separate engine behind a seam: Tokio hands over the socket, Derived Keys and the sans-IO core instance, and the engine executes the core’s Send Plans at the core’s deadlines. Which engine model to use (a dedicated thread per Test Connection with sleep-then-spin pacing, or one pinned single-threaded runtime per core with a high-resolution timer) is chosen by a prototype with pass criteria fixed before it is built, including a Server-scale case of about 200 connections at 50 Mbps. Kernel pacing (SO_TXTIME with the fq qdisc) needs root and a non-default qdisc, so it is opt-in only. A follow-up ADR records the chosen model and its measurements.

Consequences

  • Nothing the Control Phase does may block the Data Phase Engine, and the engine must release admission state on every exit path, including a panic.
  • Because the core is sans-IO, changing the engine model later touches only the runtime layer.

Sending Rate Structure sizes are IPv4-based; IPv6 senders subtract 20 bytes

RFC 9946 has two SHALLs that collide on IPv6. §8.1 says the Client SHALL use the transmission parameters the Server sends, unchanged, and §6.1 caps IP packet sizes at 1250, 1500 or 9000 bytes. The RFC doesn’t say whether the payload sizes in the Sending Rate Structure assume IPv4 or IPv6 headers. OB-UDPST stores IPv4-based payload sizes (IP size minus 28) and has whichever side sends the load subtract 20 bytes on IPv6. That keeps packet sizes and rates the same on both address families. We adopt the same convention for both our Client and our Server, and record it as a reconciliation of the two SHALLs rather than as a free interpretation. Our Client can’t tell an OB-UDPST Server from any other, so it has to subtract every time. A Server that sent IPv6-based sizes would then get packets 20 bytes too small, and a Server that sent IPv4-based sizes without the Client subtracting would break the §6.1 limits.

Consequences

  • IPv4 vs IPv6 is decided from the actual IP version of each Test Connection’s packets, never from the socket family. An IPv4 peer on a dual-stack socket (::ffff:a.b.c.d) is IPv4.
  • A third-party Client that follows §8.1 literally will send 1270-byte IPv6 packets when talking to our Server, which breaks §6.1. This is documented, not prevented.

Congestion uses a back-to-back Errored Interval counter and a latched Congestion Confirmed state

RFC 9946 §5.1 requires Congestion to be inferred from Errored Intervals “in all of the consecutive intervals”, while the Y.1540 Amd2 / TR-471 flowcharts (and OB-UDPST, which follows them) keep a single SlowAdjCount that is reset only on a fast increase, so “errored, hold, errored, hold, errored” also confirms congestion. We follow the RFC’s strict reading, but not by resetting SlowAdjCount on every non-errored interval: doing that would let algorithm B re-enter Fast Mode after Congestion was confirmed and oscillate, which RFC 9946 §7 forbids (“the single-step mode continues after the first inference of ‘congestion’”). Instead a separate counter of back-to-back Errored Intervals, reset by any non-errored interval, sets a latched Congestion Confirmed state when it reaches SlowAdjThresh; algorithm B never clears the latch, and algorithm C clears it only on a Fast-Mode Retry. A Lost Status Backoff event counts as an Errored Interval (amended 2026-09-25, see below).

Consequences

  • Our Server confirms Congestion later than OB-UDPST on paths with intermittent errors, so the two can report different capacities on the same path; this is a recorded OB-UDPST Difference, and interop tests compare each result against a known bottleneck rather than against each other.

Amendment (2026-09-25): Lost Status Backoff counts as an Errored Interval

This ADR first had a Lost Status Backoff event take one row off without counting as an Errored Interval, which departed from RFC 9097 §8.1: the rate decreases “following the same process” as for errored feedback, “with the same load rate adjustment algorithm variables in their current state”, so that “lost status feedback messages … can result in rate reduction and congestion confirmation”. The maintainer chose to avoid the deviation. A backoff now adds to the back-to-back Errored Interval counter, so the third in a row (about 290 ms without a Status PDU at the defaults) confirms Congestion with the one-time drop of 3 × highSpeedDelta rows. The cost is that a short burst of lost feedback can confirm Congestion early and lower the result. Proved by crates/udpstp-core/tests/lost_status_backoff.rs.

Transport errors are classified on both receive and send, not suppressed

ICMP-driven errors reach UDP sockets on every OS, and on Linux they reach the send path too: once a Test Connection’s socket is connected (after the first authenticated Activation Request), a pending ICMP error makes the next send/sendmmsg fail and the whole batch is lost. Windows can disable its port-unreachable reset only through an ioctl that no vetted crate exposes except noq-udp, and that crate’s switch also disables WSAENETRESET, Windows’ only time-exceeded signal. So instead of suppressing errors, every receive and send path on every OS runs one classifier: would-block and interrupted are retried; ICMP-type errors (connection refused or reset, host or network unreachable, Windows 10052) are counted and ignored, and on the send path the same batch is retried once because the failed call consumed the error; EMSGSIZE becomes a path-MTU event and time-exceeded a hop-limit event, both passed to the core so results can report them (RFC 9946 §5, §10; spec story 32); only local errors (bad socket, invalid argument) are fatal.

Consequences

  • On Linux an off-path attacker who spoofs port-unreachable messages matching a connected Test Connection can still cost batches; the send retry limits the damage, and ICMP errors seen during the Data Phase are flagged in the results so a biased capacity is visible.
  • Time-exceeded is read from the socket error queue on Linux (IP_RECVERR, drained so it cannot fill the receive buffer), from 10052 on Windows, and is not available on macOS, where the socket doesn’t report it apart from other errors.
  • noq-udp’s Windows reset switch is not used.
  • The core gains an input for transport errors on a socket.
  • On Linux a pending error is returned before datagrams already queued; on Windows in arrival order. Neither loses queued datagrams.

The Server learns each Setup Request’s arrival address through nix, or quinn-udp on Windows

The Setup Response must leave from the local address the Setup Request arrived on, from the Standard Port (RFC 9946 §6.2.2: a reply from the new Ephemeral Port would be blocked by the Client’s firewall), and the Test Connection’s ephemeral socket is bound to that address. On a wildcard listener that needs per-datagram destination addresses. On Linux and macOS nix (already a dependency) reads them and sets the source address on send (IP_PKTINFO / IPV6_PKTINFO), and also supplies the received TOS and truncation facts the core’s datagram metadata needs. Windows has no safe path except quinn-udp’s RecvMeta::dst_ip / Transmit::src_ip, so quinn-udp is allowed on Windows for the standard-port listener only, with its defaults. This amends the earlier decision not to use quinn-udp, whose reasons (it overwrites DSCP and always sets DF) do not matter for small Setup PDUs on the listener; the Data Phase stays on nix, rustix and socket2. quinn-udp was chosen over its fork noq-udp for maturity and because of ADR 0006. The Server runs two listeners, IPv4 and IPv6-only, so no IPv4-mapped addresses reach the listener.

Consequences

  • The core interface gains an optional source address on outgoing datagrams and a scope id on socket-open requests (for IPv6 link-local Clients); the received TOS in datagram metadata becomes optional, since Windows and macOS cannot always supply it.
  • Datagrams queued before quinn-udp’s socket state is created arrive without a destination address and are dropped; Clients retry.
  • Untested and tracked: Windows multi-homed hosts (a reply’s source address can fail with WSAEINVAL under the strong-host model), port shadowing of the listener, and macOS arrival addresses for dual-stack IPv4.

The Data Phase Engine: a pool of Engine Threads on a Linux Server, one per Test Connection elsewhere, with bounded catch-up

Status: Accepted (2026-09-23), amended by ADR 0011 and ADR 0012, and rewritten on 2026-10-01 to record the implemented pool model. The frozen Data Phase interface, bounded catch-up, over-rate policing, timer slack of 1 ns and the CLOCK_MONOTONIC_RAW Test Time Base stand as first decided.

The Data Phase prototype compared two engine models:

  • A: a thread per Test Connection.
  • B: one Engine Thread per core serving many Test Connections from one epoll loop.

Each was measured with and without a spin tail, on a WSL2 host, in two rounds.

The models differ at Server scale. On a single Test Connection they are indistinguishable. With A confined to the same four cores, B:

  • skipped 0.006 % of datagrams on 200 × 50 Mbps and 0.4 % on 20 × 500 Mbps, where A skipped 2.6 % and 12.7 %;
  • used 20–30 % less thread CPU;
  • ran 4 threads instead of 20–200.

A was better where high-rate Test Connections shared a core: on eight 1.25 Gbps Test Connections, and on one core carrying a 1 Mpps receive beside a sender, where B protected the receive and its sender skipped more.

The first implementation ran A everywhere (one thread per Test Connection, with batched sends and receives and a ppoll wait on its socket), since on a single Test Connection the models tie. On a real host it fell behind. In the home lab’s Server-capacity check, at a 500-byte Load PDU every 1 ms per Test Connection, confined to one core’s two logical CPUs, udpstp’s Server carried 125 Test Connections Downstream where OB-UDPST’s carried 150: at 150 it left 6 % of its Load PDUs unsent with both logical CPUs busy and about 143,000 wake-ups a second. A profile put its extra cycles in switching among its threads, entering system calls and reading clocks.

With the pool, the same check read “not behind” in both directions. Downstream on the same two logical CPUs, udpstp’s Server carried 150, the check’s top, on 0.86 busy logical CPUs and about 78,000 wake-ups a second; Upstream on one logical CPU it carried 150 on 0.34, where the thread per Test Connection had needed 0.93 and about 161,000 wake-ups a second. A single-Test precision check, one Test Connection Downstream at rows 500 and 900, found the pool no worse than a thread per Test Connection in Skipped and Withheld Load PDUs, Sub-Interval shortfall, send lag or Burst lateness.

On a Linux Server we use a pool of Engine Threads:

  • as many as the CPUs the process may use (std::thread::available_parallelism, which counts its CPU affinity and cgroup quota), or as an experimental override sets; started as Test Connections arrive; unpinned, so a Server administrator places them with the service’s CPU affinity;
  • a new Test Connection goes to the Engine Thread serving the fewest, and stays there;
  • every socket of an Engine Thread in one edge-triggered epoll set; a deadline heap; a ready list served at most 256 datagrams per Test Connection per turn, so one flooded socket can’t starve pacing; sends before reads in every turn, and a turn’s sends bounded to 1 ms; commands taken every turn, and readiness collected in every turn that runs without waiting, so a saturated Engine Thread still reads;
  • the wait is ppoll on the epoll fd and an eventfd, to the earliest deadline at nanosecond resolution, waking exactly at each deadline by default;
  • the receive hold-off is the Engine Thread’s: after a turn that read, when more than one datagram is due within it, arrivals don’t end the next wait until a fixed end: at most 250 µs, and less than any of its sockets’ quarter of the time its receive buffer holds by its longest recent turn;
  • the clock is read once as a pair when a turn starts, then the Test Time Base alone wherever the core judges or stamps time, and a pair after each read that returned datagrams, none around a read that returned nothing;
  • the Engine Thread drains its sockets’ error queues;
  • timer slack of 1 ns; no spin tail and no idle polling. Both cost CPU at scale and the round-2 data showed no benefit.

The Client, and every endpoint on Windows and macOS, run one Engine Thread per Test Connection over the same per-Test-Connection code, reading the clock as they always have. A Client runs few Test Connections and needs precision at high rates, where A was the better model. Servers at scale run on Linux; Windows has no nanosecond readiness wait through a safe crate, and a macOS pool, which kqueue would allow, isn’t worth building until a macOS Server needs scale.

Receive uses header-only recvmmsg with MSG_TRUNC, with per-datagram kernel timestamps and TOS. It met 1 Mpps on one physical core with the softirq on the sibling hyperthread. GRO stays optional: it couldn’t be tested fairly on veth.

A Transmitter that falls more than one interval behind sends only its most recent missed Burst. Amended by ADR 0012: a missed Burst less than the catch-up budget late is still sent, so Transmitters with a 100 µs interval catch up after a late timer; older Bursts are still skipped.

  • Older Bursts are Skipped Bursts: counted, reported and deducted from the sender bit rate.
  • Unbounded catch-up was rejected. That put up to 8 % more than planned into single Sub-Intervals, which inflates the RFC 9097 maximum on paths that aren’t the bottleneck.
  • SO_TXTIME kernel pacing is not adopted. It moves lpduTime about 250 µs away from departure, and adopting it would change lpduTime’s meaning.

The core’s Data Phase interface is frozen as the prototype’s amended candidate:

  • a heap-free Send Plan that holds at most one earlier Burst’s Deferred Datagrams plus one new Burst;
  • write_datagrams writes headers (and random payload when requested) into a runtime-owned buffer of twice the largest Burst;
  • report_sent is the only backpressure channel;
  • receive batches carry a clock pair and per-datagram kernel timestamps and TOS;
  • Status PDUs carry their kernel receive time;
  • transport errors include time-exceeded;
  • there is a stop input and an event output;
  • poll_burst keeps a horizon argument that must be 0 while kernel pacing is off.

The same contract suite passes against every executor: the simulator’s, the per-Test-Connection executor and the pool, with one Test Connection and with 32 on one Engine Thread. It covers spacing, ordering, partial sends and the timestamp domain; the simulator’s tests cover fresh lpduTime on Deferred Datagrams and bounded catch-up under stalls, and a counting allocator checks that 8 Tests on one Engine Thread allocate nothing in their steady state.

Considered Options

  • A thread per Test Connection on every platform, tuned (fewer clock reads and empty reads per wake-up). It is what ran first, and the home lab’s check found it short on one core. Tuning it keeps one switch per Burst per Test Connection, which a shared Engine Thread avoids whenever several Bursts fall due together; the pool was chosen over a tuning pass, and the check then read “not behind” with it.
  • Pinned Engine Threads, one per core, as first decided. The example systemd unit’s SystemCallFilter=~@privileged @resources denies sched_setaffinity with SIGSYS, and no measurement showed pinning helping; the service’s CPU affinity places an unpinned pool where the Server administrator wants it. Revisit with a measurement that shows migrations costing precision.
  • timerfd in the epoll set. Each turn re-arms it, a system call more per wake-up than a ppoll timeout, and keeping arrivals from ending a wait during the hold-off would need epoll_ctl calls or a nested epoll set.
  • The epoll wait’s own timeout. epoll_wait takes milliseconds; epoll_pwait2, which takes nanoseconds, needs Linux 5.11, and the Linux builds run on 4.18 and later.
  • A hold-off per socket in a shared Engine Thread, taking each socket out of the epoll set during its hold-off: two system calls per hold-off, and the other sockets still wake the thread.
  • Keeping heavy Load Receivers off pacing Engine Threads. Sends go first in every turn and each Test Connection’s reads are bounded, which protects pacing at the rates measured; a row changes every Trial Interval in a Search Test, so a rate at placement says little. The prototype’s evidence stands: an Engine Thread saturated by receiving skips its Load Senders’ Bursts. Revisit if one Engine Thread saturates while others idle.
  • A pooled Client. Few Test Connections, and precision at high rates is where A measured better.
  • A pool on Windows and macOS. Windows’ readiness waits through mio round to milliseconds; macOS’s kqueue would do, but no macOS Server needs the scale yet.
  • Sends batched across Test Connections with io_uring. Submitting needs unsafe in the safe wrappers, and its system calls are outside the unit’s filter; sendmmsg takes one socket.
  • A spin tail, or an idle-poll threshold. Round 2 showed no benefit for their CPU.
  • Grouping wake-ups by default, as OB-UDPST’s fixed 100 µs tick does: an Engine Thread whose next deadlines fall within a window of the earliest waits for the last of them and serves them in one turn, each Burst at most the window late and still stamped at departure. It is built, as an experimental setting with no grouping by default. With a 50 µs window the check also read “not behind” and the Server woke about 60 % less often at 125 Test Connections, but what grouping costs Test Connections sharing an Engine Thread could only be shown on loopback, and the home lab’s laptop gave no loopback setup that ran without Skipped Bursts (episodes in which the host slowed every operation, not the pool), so its cost stayed unmeasured. Revisit with a measurement of that cost.
  • The Engine Thread count as a stable Server setting. It is an experimental override for measurements; Server administrators size the pool with CPU affinity or a container’s CPU limit, which it follows.

Consequences

  • Protocol and runtime consequences:
    • The Test Time Base is CLOCK_MONOTONIC_RAW on Linux: CLOCK_MONOTONIC is slewed. Waits are converted to CLOCK_MONOTONIC and re-checked against RAW on wake-up.
    • Receive timestamps are converted with clock pairs taken before and after each read.
    • Bounded catch-up, Skipped and Withheld Bursts, and a sender-limited warning join the Data Phase rules.
    • The Server polices a Load Sender that runs above its assigned rate: it stops the Test Connection after 3 Trial Intervals above 1.5×.
    • The late-burst metric gives way to over-rate, skip and shortfall gates, all re-judged on real hosts.
    • The Concurrency section says a Linux Server runs a pool of Engine Threads sized to its allowed CPUs, and everything else one Engine Thread per Test Connection.
  • Recorded OB-UDPST Difference. OB-UDPST drops unsent datagrams, optionally rolls the sequence number back, and reschedules from “now” with no catch-up. It is recorded as the OB-UDPST Difference missed-bursts-catch-up-or-are-skipped.
  • Engine Threads:
    • They catch panics around each Test Connection’s work in a turn, so a panic retires one Test Connection and releases its Admission (ADR 0003), not the others its Engine Thread serves.
    • Every Test Connection handed to an Engine Thread reports its end, whatever happens to the thread: its report is sent when it is dropped, if it hasn’t been sent already.
    • They are supervised from the Control Phase, which learns every end from the Engine Thread’s report, after that Test Connection’s last Data Phase event.
    • The release profile keeps panic = "unwind".
    • In the pool, a stop reaches an Engine Thread at its next turn, or at once through its eventfd while it waits.
    • A Test Connection’s end does nothing on its Engine Thread that could stall the others: reading the process’s UDP tables for its Receive Buffer Overflows happens off it.
    • The Server logs how its engines run once as it starts: the executor, the most Engine Threads it may start and the grouping window.
  • Settings. The Engine Thread count, the grouping window, the hold-off, the catch-up budget and the kernel send timestamps are passed to the Data Phase Engine when it starts; the binary reads the experimental environment variables that set them, which stay experimental (ADR 0013).
  • A Data Phase socket leaves the Control Phase’s reactor when its Data Phase starts, so its traffic never wakes the Control Phase.
  • The hold-off’s buffer share counts payload bytes, not the memory the kernel charges for them, so on loopback and veth, which charge a datagram split from a UDP GSO message far more than its size, a fast Load Receiver sharing an Engine Thread with Load Senders can overflow its receive buffer; over a NIC the share holds.
  • Per-datagram random payload needs a generator cheap enough for 1 Mpps. A naive one saturated the Engine Thread.
  • Deployment guidance. Hyper-V guests should avoid CPU 0. Timer slack must be 1 ns. Receive softirq is steered off the Engine Threads’ logical CPUs where the host allows; an unprivileged process can’t do it itself. A Server confined with the service’s CPU affinity or a container CPU limit runs as many Engine Threads as it is given CPUs.

authUnixTime is checked within ±5 s inclusive modulo 2^32, the first Setup Request is signed with the Client key, and digests cover the PDU as received

RFC 9946 leaves several details of mode 1 open. We resolve them as follows.

The time window. A received authUnixTime is acceptable when it is within 5 seconds of the receiver’s wall clock, in whole seconds, with both edges inclusive. That matches the RFC’s “10-second tolerance window (+/- 5 seconds)” (§5.3.1) and OB-UDPST (udpst_control.c:2280-2286). The difference is taken modulo 2^32 as a signed 32-bit value, so times on either side of the field’s 2106-02-07 06:28:16 UTC wraparound compare correctly. OB-UDPST converts to a 64-bit time_t and would reject every PDU after the wrap. Senders put the low 32 bits of the wall clock’s seconds in the field, and the KDF context is the decimal form of that 32-bit value (ADR 0001). Both ends check the window, on requests and on responses.

Which key signs the first Setup Request. The Client derives the Derived Keys before sending, from the authUnixTime its first Setup Request carries, and signs that request with the Client Authentication Key. The Shared Key is never used directly. This is the only reading consistent with “this key SHALL only be used as input to the KDF” (§5.4) and “derived … at the client prior to transmission” (§5.4.1), and OB-UDPST does the same. The Server derives from the received request’s authUnixTime before verifying, and discards the keys if the digest fails.

keyId in responses. The Client selects no key from a response’s keyId. It verifies every response with the Derived Keys of its own Setup Request. The Server echoes the request’s keyId in its responses.

What the digest covers. The receiver computes the HMAC over the PDU exactly as received, with only authDigest and checkSum zeroed. Reserved fields are included even though their values are otherwise ignored (“the entire PDU”, §5.3.1). A decoded PDU drops reserved fields, so it is never re-encoded for verification. OB-UDPST signs the raw buffer and carries values in Activation reserved2 and reserved4 when some options are on.

Activation in mode 1. The keys, authMode and keyId are fixed by the Setup Request and kept per Test Connection (§5.4.1). An Activation Request must carry the Setup’s authMode and keyId. Both are checked before the digest, as part of “authMode … appropriate” (§6.2 step 3), and a mismatch is dropped silently like any other verification failure (§7.2.1). OB-UDPST doesn’t check keyId, but its Client always sends the Setup’s, so this doesn’t affect interop. Responses, BADPARAM included, carry the Setup’s authMode and keyId, a fresh authUnixTime and the Server Authentication Key’s digest.

A response that fails verification is ignored, not fatal. The Client drops a Setup or Activation Response that fails verification and keeps waiting; the test initiation timer decides the outcome. §5.3.1 says the receiver “SHOULD NOT continue with the Control phase”, and ignoring the response satisfies that without letting a forged datagram end a genuine Test. OB-UDPST ends the Test at once.

Consequences

  • Proved by crates/udpstp-core/tests/setup_mode1.rs and activation_mode1.rs:
    • accepted at ±5 s, dropped at ±6 s;
    • accepted across the 2106 wraparound;
    • scripted peers check which key signs each PDU in both directions;
    • a non-zero reserved field signed as sent is accepted;
    • an Activation Request with another authMode or keyId than its Setup is dropped silently.
  • The requirement entries for §5.3.1 and §5.4.1 reference this ADR.
  • A replay of a Setup or Activation Request inside the window is dropped by the replay cache (crates/udpstp-core/src/replay.rs). Mode 2 Status PDUs aren’t in it: a replayed one has a stale spduSeqNo, so it can’t change the rate or deliver STOP.

Key Table rows follow RFC 7210 with an HMAC-SHA-256 AlgID, lifetimes checked on our clock, and an Operator extension

RFC 9946 §5.4 requires a Key Table with RFC 7210’s AdminKeyName, LocalKeyName, KDF, AlgID, Key and send/accept lifetimes, but leaves how they are used to the implementation. We decide as follows.

AlgID and KDF. The KDF column is HMAC-SHA-256, the IANA “KeyTable KDFs” name. It stands for the NIST SP 800-108 counter-mode construction with the UDPSTP label (§5.4.1, ADR 0001). The “KeyTable AlgIDs” registry has no value for the authDigest algorithm, so we use the unregistered name HMAC-SHA-256, mirroring the KDF. They are the only values the table accepts. LocalKeyName is the one-octet keyId (§5.4).

Lifetimes. A send or accept lifetime is a pair of UTC bounds in whole seconds, both inclusive, as RFC 7210’s YYYYMMDDHHMMSSZ format has. Either end may be open. A lifetime that ends before it starts is rejected when the row is loaded.

  • Server: it uses accept lifetimes only. At Setup it looks up the row whose LocalKeyName is the request’s keyId and whose accept lifetime includes the Server’s own wall clock (RFC 7210 §3). If rows exist for the keyId but none is in its accept lifetime, the request is dropped silently like any authentication failure (SilentRejectionReason::KeyOutsideLifetime; Troubleshooting Mode, answers it as CHSR_CRSP_AUTHFAIL).
  • Established Test Connections: they keep their Derived Keys when the row’s lifetime later ends (RFC 7210 §3: “exceeding the lifetime does not have any direct impact”).
  • Client: it uses send lifetimes only. It sends with the row whose send lifetime includes its wall clock, preferring the most recent SendLifetimeStart (RFC 7210 §3’s rollover rule). Rows tied for the newest start (every row of an OB-UDPST key file, which has no lifetimes) make the choice ambiguous. The Client then doesn’t guess: it fails unless a keyId is pinned with --key-id, like OB-UDPST’s -y option. OB-UDPST instead falls back to keyId 0 (DEF_KEY_ID, udpst.c read_keyfile). With no valid row it doesn’t start the Test.
  • No clock-skew grace. RFC 7210 §3 allows “some grace time for clock skew”. We add none. Overlapping lifetimes across keyIds give the same effect under the operator’s control (RFC 7210 §6).

Shared keyIds. keyId is only one octet, so rotation reuses keyIds.

  • Two rows may share a keyId only if neither their accept lifetimes nor their send lifetimes overlap. Otherwise the second row is rejected when the table is loaded.
  • For accept lifetimes this is necessary: the Server finds a row by keyId alone, and RFC 7210 §3 selects a key only “if one key matches”.
  • For send lifetimes it is for symmetry and simplicity. One key file can serve both roles, and a pinned keyId then always has at most one valid row.
  • Caches must follow this. The Derived Key cache keys on the row and authUnixTime, not the keyId, so a hit can’t outlive its row’s accept lifetime.

Extension to RFC 7210. A row’s operator column (true, or false when absent) marks an Operator Key, and an Operator row may carry its own maximum Test Interval. Both are needed for Operator-only Fixed-Rate Tests and for Tests longer than 60 s. RFC 7210 has no column for them, so this is an extension that belongs to our key file format.

The columns RFC 9946 §5.4 doesn’t list (PeerKeyName, Peers, Interfaces, Protocol, ProtocolSpecificInfo, Direction) are not modelled.

Consequences

  • Proved by crates/udpstp-core/tests/key_table.rs:
    • expired and not-yet-valid keys are dropped silently, and the edges are accepted;
    • rollover between two rows with one keyId works;
    • overlapping and backwards lifetimes are rejected;
    • the Client picks the newest valid send key, and refuses to guess between equally new ones unless a keyId is pinned.
  • OB-UDPST key files have no lifetimes. Loading one gives rows valid at all times, so a Client with several keys needs a pinned keyId.
  • Rotating within one keyId leaves no overlap, so a Client and Server whose clocks disagree near the switch fail until they agree. A smooth rotation gives the new key a new keyId whose lifetimes overlap the old one’s, and retires the old keyId later (RFC 7210 §6).

Over-rate policing judges against the highest rate assigned in the last second

Status: Accepted (2026-09-25). It amends ADR 0008.

ADR 0008 has the Server police an Upstream Load Sender: after 3 Trial Intervals in a row above 1.5 times its assigned rate, the Server stops the Test Connection. That was written for a fixed rate. Once algorithm B moves the row every Trial Interval, “its assigned rate” is ambiguous. The Client switches to a new Sending Rate Structure about a round trip after the Status PDU carrying it leaves. What already queued at a bottleneck keeps arriving at the bottleneck rate until the queue drains, which takes at least upperThresh (90 ms by default) whenever DelayVar confirmed the Congestion. After algorithm B’s one-time drop of 3 × highSpeedDelta rows, the next few Trial Intervals therefore receive more than 1.5 times the new row on any path whose buffer holds more than about one Trial Interval. An impairment test measured an Upstream search through 20 Mbit/s with a 200 ms buffer being stopped as OverRate after 19 Trial Intervals, although the Client did nothing wrong.

We police each Trial Interval against the highest rate the Server assigned within the last second, the current one included. One second covers a round trip plus the drain time of a queue at upperThresh plus a Trial Interval on any path the Test can measure. A Client that really sends too fast is still stopped: its rate exceeds 1.5 times every rate assigned in the window, and at most one second later than before.

Consequences

  • A Client may run at up to 1.5 times the highest rate of the last second for up to a second after a decrease. That bound is still far below what an unpoliced Client could send.
  • The test an_upstream_search_through_a_deep_buffer_is_not_stopped_over_rate (crates/udpstp-core/tests/algorithm_b.rs) proves the fix; the_server_stops_a_client_sending_over_its_rate (tests/upstream.rs) still proves the policing.
  • Alternatives considered: pausing policing for a fixed time after each decrease (more state, same effect), or a window sized from the measured RTT (the RTT isn’t known before the first samples, and a bottleneck’s drain time isn’t the RTT).

Catch-up is bounded by a time budget

Status: Accepted (2026-09-26), amended 2026-09-27. It amends ADR 0008.

Bounding catch-up to one Sending Rate Structure interval makes the allowed lateness depend on the selected row. Rows of 100 Mbps and up send a Burst every 100 µs, which is shorter than a portable timer can reliably sleep. Skipping every Burst older than one interval can therefore make the host limit the Test before the network does.

A missed Burst less than the catch-up budget late is still sent. One at least the budget late is a Skipped Burst. The most recent missed Burst always goes, as under ADR 0008. The runtime supplies the budget through DataPhase::set_catch_up_budget. Windows uses 2 ms. Other platforms use the core’s default of 1 ms. The Windows budget was increased from 1 ms after timer and loopback diagnostics showed that 1 ms did not cover ordinary late wake-ups.

Decision

  • Keep the Send Plan interface. poll_burst returns late Bursts one per call, oldest first, each with its scheduled time. A plan holds Deferred Datagrams plus at most one new Burst. horizon remains 0 and report_sent remains the backpressure channel.
  • Shift scheduled traffic without adding traffic. Catch-up sends only planned Bursts. A late wake-up can move traffic into the next Trial Interval, but does not raise the long-run scheduled rate. The time bound limits that shift. At the default 50 ms Trial Interval, 1 ms is 2 % and 2 ms is 4 % per Transmitter. At the shortest 5 ms Trial Interval those figures are 20 % and 40 %. This is a single shift rather than sustained excess under ADR 0011.
  • Keep reception responsive. The portable engine reads between Send Plans, so catching up does not defer a Status PDU’s userspace receive timestamp until every late Burst has been sent.
  • Bound a turn as well as catch-up. The engine sends for at most 1 ms per turn before processing its other work. This prevents a host that always takes longer than one interval to send from remaining in the send loop indefinitely.

Considered options

  • One interval plus a spin tail. Spinning can improve deadline precision, but costs CPU for each Test Connection. ADR 0008 rejects it at Server scale.
  • Coalesce several Bursts into one plan. This changes the Send Plan contract and is unnecessary for a time budget.
  • Scale the budget to trialInt or the row. This can shrink it below the timer lateness it needs to cover. A fixed time bound keeps the decision independent of the selected row.

Consequences

  • Late Burst and Skipped Burst in GLOSSARY.md refer to the catch-up budget rather than one row interval.
  • Skipped Bursts remain counted, reported and deducted from the IP-Layer Sender Bit Rate.
  • A longer stall still skips old traffic rather than releasing every missed Burst at once.
  • The OB-UDPST Difference remains. OB-UDPST drops unsent datagrams and reschedules from now without catch-up.

udpstp’s stable interfaces from 0.1.0

Status: Accepted (2026-09-26). Amended on 2026-11-01: the CLI, the TOML settings and the Server’s JSON log settle during 0.x and become stable at 1.0.

Measurement platforms and Operators script against udpstp: they parse its JSON results and the Server’s JSON log, branch on its exit status, generate its configuration and Key Tables, and join pseudonymized addresses across days. From 0.1.0 they need to know which of these they can build on and how a change reaches them. The rules apply to the udpstp binary; the library crates are a separate case (below). docs/interfaces.md is the user-facing summary.

Decision

Stable from 0.1.0. These change only with a changelog entry and a deprecation period:

  • exit statuses: the rule behind them and every value assigned so far (below);
  • the JSON results: key names, types and enumerated values, but not the free text inside them (ErrorMessage, ErrorMessage2, a warning’s Message). The OB-UDPST part tracks OB-UDPST v9.1.0’s schema, except the OB-UDPST Differences recorded in docs/conformance/records/client.toml;
  • the key file formats (below), and the pseudonym key file: hexadecimal digits, at least 16 bytes, with whitespace around them and nothing else;
  • which output goes to stdout and which to stderr: the Client’s JSON results alone on stdout with --format json, its text results and progress on stdout otherwise, errors and warnings about the host on stderr; the Server’s log on stdout, its startup errors and warnings on stderr, --print-config on stdout; with a log file set (log_file, ADR 0014), the log and the startup’s errors and warnings (StartupError, StartupWarning) go to the file, the latter to stderr too;
  • the pseudonymized-address algorithm: anon- and the lowercase hexadecimal of the first 8 bytes of HMAC-SHA-256, keyed with the pseudonym key, over the IP address’s octets (4 for IPv4, an IPv4-mapped IPv6 address taken as its IPv4 address; 16 for IPv6); truncate shows the IPv4 /24 or IPv6 /48.

Settled during 0.x, stable from 1.0. These may change in any 0.x minor release, with a changelog entry under Changed or Removed and no deprecation period. From 1.0 they follow the same rules as the list above:

  • the CLI and the Server’s TOML settings: option and key names, value spellings and defaults;
  • the Server’s JSON log: event names, field keys and enumerated values. The keys every line has (timestamp, level, message) come from tracing-subscriber; message is free text.

Scripts and measurement platforms mostly read the JSON results and branch on the exit status, which are stable from the first release. Command lines, settings and log names haven’t met users yet. Letting them settle during 0.x means early feedback can fix them without carrying an alias for every rename.

Not stable: all human-readable text (the text results, progress lines, warning and error wording, --help, the human log’s message text and layout); the --version string’s format; the test-only environment variables UDPSTP_FAULTS and UDPSTP_BLESS, and the experimental ones such as UDPSTP_ENGINE_HOLD_OFF_US and UDPSTP_ENGINE_CATCH_UP_US, which may change or go in any release.

Additions are not changes. A minor release may add options, settings, JSON keys, log events and fields, enumerated values, exit statuses and Key Table columns. A tool must ignore what it doesn’t know. An addition gets a changelog entry under Added.

Deprecating an input (from 0.1.0 a Key Table column, and from 1.0 also an option, a setting or a value spelling): for one minor release the old name is still accepted, through a serde alias or a clap alias, with a warning on stderr naming the new one, and the changelog lists it under Deprecated. An alias alone is silent, so the warning comes from the code that reads the input, as mtu’s hint does today (a hidden option, and a look at the file’s keys before the strict parse; config.rs). A name deprecated in 0.3.0 is accepted through 0.3.x and may be removed in 0.4.0, which the changelog lists under Removed.

Deprecating an output name (from 0.1.0 a JSON key or value, and from 1.0 also a log event, field or value): for one minor release both the old and the new name are emitted, and the changelog says so. Then the old one goes.

Defaults have no alias: from 1.0 a default changes in a minor release, announced in the changelog of the minor release before. A security fix may change a default, or refuse an input, without the period; its changelog entry says so.

Changes before 0.1.0 need no deprecation period: nothing is stable yet. ADR 0015 replaces Windows permission warnings with refusal at status 51 and removes the two warning reasons before 0.1.0. The additive --settings-only modifier preserves ordinary --print-config key validation. mtu became server_mtu and is refused with a hint.

Exit statuses

The ErrorStatus is OB-UDPST’s (udpst.h), and the exit status equals it, except that a Test that completed with only warnings exits 0. udpstp’s own statuses take values OB-UDPST leaves unused: 49 in its warning range, and 201 and up in its connection-error range, which uses only 200. A new status may be added; an assigned one never changes meaning (crates/udpstp/src/exit.rs):

ErrorStatusMeaningExit status
0The Test completed0
2 to 6It completed with a warning, the most severe of: 4 nothing received for 1 s, 5 the peer said it stopped receiving, 3 the peer missed Status PDUs, 2 Status PDUs never arrived, 6 invalid PDUs0
49Ours: it completed without a valid Maximum IP-Layer Capacity; the most severe warning0
50The configuration or command line is invalid50
51The key file can’t be read or is invalid51
75The Client or Server couldn’t start75
100 + cmdResponseThe Server refused the Setup Request (102 to 113); 100 for a code RFC 9946 doesn’t define; 107 or 108 for a Setup Response that failed verification or the time windowthe same
150 + cmdResponseThe same for the Activation exchange: 152 BADPARAM, 150 an undefined code, 157 or 158the same
200The Test didn’t complete: no answer in time, or the Watchdog ended it200
201Ours: the Server coerced a parameter the Client was told to refuse201
202Ours: the Server set a parameter the Client can’t run with202
203Ours: a Multi-Connection Test’s Servers gave different parameters203
204Ours: the Client user interrupted the Test, or a write to standard output failed. On Unix a Client stopped by a signal then dies of it, so a shell sees 128 plus the signal (130 for SIGINT); 204 is then the JSON ErrorStatus204, or the signal’s
255Any other error255

Key files

Forward-incompatible by design. Every file valid in a release keeps its meaning in every later one: a column is never removed or read differently, only added. A file written for a newer udpstp, with a column this version doesn’t know or a higher format, is refused whole rather than read in part, because the column it would skip may be a restriction: a lifetime, a Quota, an Operator Key’s limits. An older Server that ignored it would widen what a key allows.

A TOML Key Table may start with format = 1; a file without it is format 1. A later format number is needed only for a file that an older udpstp would misread even though it knows every column, which the rule above rules out, so it is reserved for a new format beside this one. A renamed column is an input like any other: its old name is accepted, with a warning, for one minor release. OB-UDPST’s udpst.keys format is read as OB-UDPST reads it (conformance: obudpst-key-file-reading), and follows OB-UDPST.

Names come from tables

Every log event, JSON enumerated value and warning code is written from the explicit tables in crates/udpstp/src/names.rs, never from a Rust Debug name, so renaming a core variant can’t change an output. The core’s own names are free to change.

The library crates

The published library crates, udpstp-wire, udpstp-core and udpstp-runtime, follow Cargo’s semver rules only, and aren’t a supported interface in 0.x: a minor release may break them without a deprecation period. The binary is the interface.

The container image

Publishing a GitHub release starts the separate Release container workflow, which publishes ghcr.io/pederbe/udpstp:<version> (such as :0.1.0) from its verified Linux binaries. :<version> never moves. :latest follows releases that are not marked as pre-releases, including one that ends a deprecation period, so a deployment pins :<version> and upgrades on purpose. Release artifacts and verification describes its downloads and provenance.

Considered options

  • Promise nothing until 1.0. Platforms script against the JSON results and the exit status from the first release; without a promise, every 0.x release could break them unannounced.
  • Hold every interface to the deprecation rule from 0.1.0. This was the decision until 2026-11-01. It froze command-line, settings and log names before any user had tried them, so every early fix would need an alias and a warning for a release.
  • Snapshot --help and the log’s text. Text changes for good reasons, and the specification asserts on structured output, never on text. The tests read clap’s Command model, the settings struct and the JSON log’s parsed lines instead.
  • Accept unknown Key Table columns with a warning. It lets one Key Table serve a mix of versions, but an older Server would drop a restriction it can’t read, which widens access; a warning in a log nobody reads doesn’t stop that.
  • Keep deprecated names forever. Every alias is a second spelling to document and test; one minor release gives users a release to see the warning and move.
  • Make the library crates a supported interface. Their API still changes with each feature, and the core’s sans-IO shape isn’t settled enough to promise.

Consequences

  • A change to a stable interface fails a test, and the change’s commit rewrites the checked-in list with UDPSTP_BLESS=1, where the diff shows it for review:
    • CLI options, value names, spellings and defaults, from clap’s Command model: tests/golden/cli.txt (crates/udpstp/src/interfaces.rs);
    • every TOML key, from a fully populated settings struct, and every default: server-settings.toml and server-defaults.toml; the same test checks that each Server option sets the key of its name, and that values are spelled alike in both;
    • log event names and enumerated values: names.txt, from the tables in names.rs; each JSON log line’s keys and types: log-fields.txt, and every event in the table is logged (log.rs);
    • the JSON results: completed.json, bimodal.json and extension.json (json.rs);
    • exit statuses: exit-statuses.txt (exit.rs);
    • the Key Table’s columns and format: key-table-columns.txt (keyfile.rs);
    • the pseudonym algorithm: known answers in privacy.rs.
  • Each release’s changelog lists what was added, deprecated and removed; releasing.md has the step.
  • Open: how an enumerated value is emitted under both names during its period, since one field holds one value (a second field for the new value, or the old value kept until the period ends). The first such deprecation decides it.
  • Whatever 0.1.0 ships in the JSON results, exit statuses, key files, stdout and stderr, and pseudonyms becomes stable, inconsistencies too. The golden tests still catch every CLI, settings and log change during 0.x, so each one is deliberate and has its changelog entry. Two command-line inconsistencies were settled before 0.1.0: the Client’s --checksum takes an optional boolean as the Server’s does, and the Server’s --delay-var / delay_var, the statistic it classifies by, became --delay-var-statistic / delay_var_statistic, so --delay-var names only the Client’s choice of delay variation.

The Server runs as a Windows service of its own, under a virtual account, logging to daily files

Status: Accepted (2026-10-01).

A Server meant to run unattended on Windows must be a Windows service: the Service Control Manager starts a service at boot, restarts it after a failure and stops it at shutdown. udpstp server wasn’t one. The stop-gaps all failed somewhere:

  • a Task Scheduler task’s account credentials expire after 42 days by default, it runs below normal priority and it has no standard output;
  • a third-party wrapper, WinSW or NSSM, is one more program in front of a network-facing Server to trust, ship and update, neither has had a stable release for years, and it can’t tell the Service Control Manager when the Server actually listens.

udpstp server --windows-service is the service, udpstp-server:

  • Native support, through windows-service (Mullvad’s crate), without its define_windows_service! macro. That macro, the documented way to write a service’s entry point, expands an unsafe block into the calling crate, where #![forbid(unsafe_code)] doesn’t see it (rustc doesn’t report the unsafe_code lint in another crate’s macro), so it would be udpstp’s own unsafe in all but name, which needs an ADR of its own (the specification). The entry point is written by hand instead, a safe extern "system" function that never reads the start parameters the Service Control Manager passes; the Server’s options come from the service’s command line. clippy.toml bans the macro, since the compiler won’t.
  • The same Server. The service runs the code a console runs, and reports its state: starting, running once it listens, stopping, and stopped with the exit status it would have had in a console as its service-specific exit code. It stops on the service’s stop and shutdown controls, through the same path as Ctrl+C, so its Tests get their STOP and the log its summaries (Stopping, reason ServiceStop or ServiceShutdown). It installs no console handlers: a service gets the console’s shutdown event too, before the service’s own shutdown control, and acting on it would log the wrong reason.
  • Its own virtual account, NT SERVICE\udpstp-server. Windows manages it, with no credential to expire. The Server needs no privilege, since Windows has no privileged ports, so the service keeps only SeChangeNotifyPrivilege, which every process keeps; a service otherwise holds SeImpersonatePrivilege, the usual way from a service account to SYSTEM. Its own SID lets the Key Table be readable by the Server and no one else, and lets a firewall rule be scoped to the service. LocalService would share its identity with other services, and LocalSystem is far more than the Server needs. The name is the systemd unit’s, and is costly to change: the account’s SID follows from it, and so do the ACLs, the firewall rule and administrators’ scripts.
  • The log goes to daily files (log_file), since a service has no standard output: a new file each day, the last 14 kept, through tracing-appender. Rotating one file by renaming it fails on Windows while a reader holds it open, and a size bound keeps only hours of history during a flood, which the log is kept for. The Windows Event Log was rejected: it needs an event source registered at install and a message resource, and its volume under a flood would crowd out other services’ events. With a log file set, the startup’s errors and warnings go into it too, as StartupError and StartupWarning, as well as to stderr: a service that fails to start would otherwise leave only an exit code. The service’s command line names the log file, so an error in the configuration file reaches it too.
  • Installed by a PowerShell script, contrib/windows/udpstp-server.ps1, not by a udpstp subcommand: the data folder’s ACL and the firewall rule need Win32 calls that are unsafe in Rust, or PowerShell anyway. It mirrors contrib/systemd/. Installation and reinstallation first disable startup, stop the service and clear recovery before changing data or configuration. A queued recovery restart cannot be canceled by changing its actions, but disabled startup prevents it. The settings-only preview validates configuration and extracts ports without opening keys or logs as the administrator. The real service trial starts on demand with no recovery and validates keys as the virtual account (ADR 0015). Only successful startup restores restart actions and automatic startup. Any later failure attempts disabled startup, cleared recovery and a verified stopped service, reports containment failure accurately when those operations fail, and leaves applied changes in place for remediation and rerun; it provides no rollback.

Considered options

  • A wrapper (WinSW, NSSM): no Rust code, its own log capture. Rejected for the reasons above.
  • The windows crate directly: the dispatcher with udpstp’s own unsafe, and an ADR for it, for nothing windows-service doesn’t give safely.
  • A size-rotated single file written by udpstp: a stable name to follow, but the rename fails while a reader holds the file without delete sharing, and 10 files of 10 MiB hold about 12 hours under a flood of limited log events.
  • A restricted SID type (a write-restricted token) as more hardening: possible later, after testing what the Server writes besides its log.

Consequences

  • windows-service and tracing-appender are direct dependencies (technology.md).
  • ADR 0013’s stdout and stderr rule gains the case of a log file: the log and the startup’s errors and warnings go there, and stderr still gets the latter. That stdout and stderr rule is stable from 0.1.0. --windows-service, log_file, the events StartupError and StartupWarning with the warnings’ names, and the stop reasons ServiceStop and ServiceShutdown are command-line, setting and log names, stable from 1.0 (ADR 0013, amended).
  • The firewall rule names the program and its UDP ports. An isolated service-scoped rule passed native Windows 11 IPv4/IPv6 traffic checks, and the Client from the same executable did not prompt. A console Server was denied incoming access without a prompt, and path-based dialog Block rules still overrode the scoped Allow rule (Windows firewall results). The installer retains the program/port rule; the scoped variant has been assessed on one Windows build, not adopted as a new default.
  • The end-to-end check, scripts/windows-service-check.ps1, installs a service and changes the machine, so it runs in Windows Sandbox (scripts/windows-service-sandbox.ps1) or in CI’s Windows job.

Windows Shared Key files use a bounded private-file policy and a native inspection adapter

Status: Accepted (2026-10-05). Production refusal (2026-10-06), following the separately accepted transition.

Authenticated Clients and Server administrators need accidental permission grants caught when a Shared Key file is loaded. Unix already refuses untrusted ownership and group/other read or write access. Windows now assesses permissions at startup. RFC 9946 does not prescribe a Windows algorithm. This decision supplies the supported policy for both TOML Key Tables and OB-UDPST Shared Key files.

Policy

Trust the running process account, SYSTEM (S-1-5-18) and built-in Administrators (S-1-5-32-544). A virtual service account is trusted when it is the running account, including ADR 0014’s NT SERVICE\udpstp-server. Compare binary SIDs: do not resolve account names, expand groups, consult a domain, or trust an arbitrary group because the running account belongs to it.

Assess the owner and discretionary access-control list (DACL) of a regular file on a local NTFS volume. Return owned Rust data and one of three outcomes:

  • Meets the supported policy: trusted owner and no applicable ordinary allow entry giving an untrusted identity dangerous rights.
  • Does not meet the supported policy: untrusted owner, absent/null DACL, or an applicable ordinary allow entry granting dangerous rights to an untrusted identity. This describes a policy mismatch; it does not prove effective access.
  • Cannot be verified: inspection failure, malformed or oversized descriptor, unsupported storage, ACE type, flags or rights. Unsupported data takes precedence over a mismatch so a partly understood ACL is never called verified.

Dangerous rights include reading, writing or appending file content, executing it, writing extended attributes or file attributes, changing its DACL or owner, and deleting it. File execution is included conservatively because executable sections can expose content. Reading attributes, extended attributes or permissions, and synchronization alone do not read a Shared Key. Map GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE and GENERIC_ALL to their file rights before classification using the Windows file generic mapping.

Ordinary allow and deny ACEs are supported, including inherited entries and inheritance propagation flags. An inherit-only ordinary entry does not grant access to this file. Deny entries do not cancel outsider allow entries under this structural policy, even when Windows would deny the concrete access. Unknown ACE types, including callback/conditional and object-specific entries, are unverified even if marked inherit-only. Special identities are not silently trusted: an unfamiliar SID with a dangerous allow entry is a mismatch. An empty DACL meets the grant policy with a trusted owner, but may prevent a new content read. Successful assessment never overrides an ordinary open/read failure.

Native boundary and safety

Allow udpstp’s own unsafe only inside a Windows native Shared Key permission adapter. Its safe interface accepts a borrowed open File or handle and returns owned descriptor data or an inspection error. Process-token inspection supplies the running account’s owned SID. The application policy and descriptor decoder remain safe Rust beside key-file loading; the protocol core, wire code and network runtime retain forbid(unsafe_code). This is a specific exception to the previous blanket prohibition, not authorization for other native features.

The production helper udpstp-key-permissions uses windows-sys 0.61.2 for Win32 bindings. Its crate root denies unsafe, and only src/native.rs allows it. The decoder and policy explicitly forbid it, as does every other production crate. No raw pointer or borrowed SID escapes the module, and safe callers cannot supply invalid handles. This helper is packaged with the binary’s library dependencies; it is an implementation detail under the same 0.x library-interface rules as ADR 0013.

Safety invariants:

  • The caller owns the file; its borrow lasts through each native call. Inspection neither closes nor replaces the file handle. A separately opened process token has one Rust OwnedHandle, closing it on every success and error path.
  • GetKernelObjectSecurity copies owner and DACL into a self-relative descriptor in initialized, pointer-aligned Rust storage. Query the required length first; accept at most 128 KiB, and reject any failed call or changed/out-of-range size. Rust frees this allocation. There is no OS descriptor allocation to free in the production inspection path, and no retry turns an inspection error into a pass.
  • GetTokenInformation(TokenUser) follows the same bounded allocation pattern. Validate the returned structure length and its SID pointer as an offset inside the returned buffer before reading SID bytes. Copy the bounded SID while the buffer lives. Do not dereference a returned pointer outside that buffer.
  • Parse self-relative offsets, ACL/ACE lengths and SID revision/subauthority count with checked Rust slices. Limit the descriptor to 1,024 ACEs and SIDs to 15 subauthorities; recognize only ordinary ACE layouts and documented flags. No native pointer walking is needed for descriptor decoding.
  • Check each Win32 return convention and capture the error before cleanup calls can overwrite it. File metadata, filesystem and resolved-volume checks refer to the borrowed handle. Remote volumes and unsupported cases are unverified.
  • Test-only SDDL installation owns the converter’s OS allocation and releases it once with LocalFree, including when installation fails. This writable helper is excluded from the production adapter.

Loading and limits

Open a Shared Key file for reading with FILE_SHARE_READ only. Inspect and read that same handle, preserving the existing file-size bound and wiping content buffers. An already open content writer prevents this protected open; while the reader lives, new content writers and delete/replacement opens fail sharing checks. Another reader can coexist.

This is a startup assessment. It does not freeze the security descriptor: WRITE_DAC can still change the ACL while the content reader is open. It does not protect a writable containing directory or guarantee lasting protection. Protected containing storage is a deployment assumption. Privileged bypass, pre-existing writable mappings, identity impersonation, comprehensive effective access, group expansion and unusual filesystems are outside this bounded proof.

Only a file that meets the supported policy proceeds to content reading. Policy mismatches and unverified permissions are distinct key file errors (exit status 51), with the file, reason and supported remediation, never its content. Neither outcome claims proven exposure. Client errors go to stderr; Server errors also reach its configured startup log as StartupError. Read and parse errors retain their existing handling.

Ordinary --print-config still loads and validates keys. The additive --print-config --settings-only preview validates settings and prints only their TOML, without loading keys, opening logs or listening; it cannot accompany --windows-service. The installer uses this preview to obtain ports, then the real trial start validates keys as the virtual service account. ADR 0014 defines installation failure containment. The prior warning reasons are removed before 0.1.0, under ADR 0013; no elapsed warning release is required.

Considered options

  • Runtime PowerShell: adds process startup, can be restricted by deployment policy, and inspects a path separately from the file read. Rejected.
  • Full effective-access evaluation: requires identity/group and potentially domain knowledge, plus substantially wider permission-form support. Rejected for the initial policy; a mismatch must not be reported as proven exposure.
  • Unmaintained safe ACL wrappers: do not justify moving this boundary into an unmaintained dependency. Direct maintained bindings keep the adapter visible.
  • Path inspection followed by reopening: permits object substitution between checking and reading. Rejected in favor of one borrowed handle.

Sources

Contributing to udpstp

Bug reports, interoperability reports, questions and documentation improvements are welcome. Before starting a substantial code change, open or comment on an issue so the work fits the project.

Report security problems through private vulnerability reporting, following SECURITY.md.

Build and test

Install Rust through rustup. rust-toolchain.toml selects the toolchain and components.

git clone https://github.com/pederbe/udpstp
cd udpstp
cargo build --release --locked -p udpstp
cargo test --locked --workspace
cargo xtask req

Read the glossary, architecture, testing guide and the ADRs relevant to your change. The specification records the user stories.

The project started on the latest stable Rust and supports the last three stable releases from then on. The workspace’s rust-version is the oldest supported version, which CI builds and tests. Raise it only when a newer release is needed and the oldest one leaves the last three, and describe the compatibility change.

Use the validation workflow to select relevant checks and keep generated files current. Automated checks also validate pull requests.

Make a change

Keep pull requests focused. Put protocol decisions in udpstp-core and I/O in udpstp-runtime. Use glossary terms. Add tests at the boundary where the changed behavior is observable. Tag tests proving an RFC Requirement with its ID, following the conformance guide.

Keep first-party Rust safe. ADR 0015 describes the approved, isolated Windows key permission adapter. Expanding that exception requires the maintainer’s explicit approval. Use an ADR for a consequential decision that is hard to reverse.

When work completes a feature, update the README Status table and any documentation that still calls it planned in the same change.

Pull requests

Open a pull request with a focused change. Fill in the template, describe the behavior and validation, and link the relevant public issue when one exists.

Include a concise Unreleased entry in CHANGELOG.md for user-visible work. Cover capabilities, fixes, installation, compatibility consequences and required upgrade actions. If none is needed, explain why in the pull request’s Changelog section. Follow the interface policy.

Licence and conduct

udpstp is dual-licensed under MIT or Apache-2.0. Unless you explicitly state otherwise, any contribution you intentionally submit for inclusion in udpstp, as defined in the Apache-2.0 license, is dual-licensed as above, without any additional terms or conditions.

Everyone taking part follows the code of conduct.

Citing udpstp

If you use udpstp in research or a publication, please cite it. The repository’s CITATION.cff holds the citation, which GitHub also offers as APA and BibTeX:

cff-version: 1.2.0
message: "If you use udpstp in research or a publication, please cite it as below."
type: software
title: "udpstp: the UDP Speed Test Protocol (RFC 9946) in Rust"
abstract: >-
  A memory-safe, RFC 9946-conformant implementation of the UDP Speed Test
  Protocol, which measures Maximum IP-Layer Capacity as defined in RFC 9097.
authors:
  - family-names: Bergan
    given-names: Peder
repository-code: "https://github.com/pederbe/udpstp"
license:
  - MIT
  - Apache-2.0
keywords:
  - RFC 9946
  - RFC 9097
  - UDPSTP
  - IP capacity
  - network measurement
  - speed test
  - Rust
references:
  - type: standard
    title: "The UDP Speed Test Protocol (UDPSTP) for One-Way IP Capacity Metric Measurement"
    identifiers:
      - type: doi
        value: 10.17487/RFC9946
    authors:
      - family-names: Morton
        given-names: A.
      - family-names: Ciavattone
        given-names: L.
      - family-names: Geib
        given-names: R.
    year: 2026
    institution:
      name: Internet Engineering Task Force

Changelog

User-facing changes in udpstp, including capabilities, installation and compatibility.

[0.1.2] - 2026-11-06

Fixed

  • Align the crates.io overview with the project README and update the documentation for published releases.
  • Refresh the website’s product overview and link Fastmash, Ripscope and Esdieff from About and the footer.

[0.1.1] - 2026-11-05

Added

  • A Client and Server for the UDP Speed Test Protocol in RFC 9946, using the Maximum IP-Layer Capacity method in RFC 9097.
  • Authenticated Upstream and Downstream Tests, Multi-Connection Tests, and Operator-only Fixed-Rate Tests. Mode 2 is the default. Mode 1 is available when selected, and Unauthenticated Mode requires both ends to opt in.
  • Setup and Activation exchanges, Load Rate Adjustment Algorithms B and C, Lost Status Backoff, the Sending Rate Table, optional header checksums, the STOP exchange and protocol watchdogs.
  • Maximum IP-Layer Capacity per Capacity Mode, with loss, delay variation, RTT and sender bit rate. Text and JSON results preserve measured Sub-Intervals from incomplete Tests and explain warnings and partial completion. JSON supports OB-UDPST-compatible fields and a separate udpstp extension.
  • Adaptive terminal layout, compact results and --color auto|always|never. Automatic color handles stdout and stderr independently. JSON, printed configuration and persistent logs stay plain.
  • TOML Key Tables with lifetimes, Operator Keys and Quotas, plus OB-UDPST key files. Unix ownership and permissions are checked before reading keys. Windows requires the supported local-NTFS private-file policy and distinguishes a policy mismatch from unverifiable permissions.
  • Client-selected Server addresses, with explicit authorization of alternate addresses for authenticated Tests. Server admission limits, replay protection, Setup Request budgets and rate-limited diagnostics.
  • Server TOML configuration, command-line overrides, validated configuration previews, structured logs, periodic statistics and address-privacy controls. Settings-only previews open no key files, log files or listeners.
  • A systemd unit, a Windows service installer with a dedicated virtual account, and container build and publication workflows for the Server. Services require configuration before use.
  • Available Linux batched I/O and kernel UDP fast paths, receive timestamps and Receive Buffer Overflow reporting. Experimental send timing diagnostics are separate from stable interfaces.
  • RFC requirement traceability, Conformance Records, deterministic protocol simulation, fault injection and interoperability tests with OB-UDPST v9.1.0 in the documented scope.
  • Release packaging and installer sources for Linux, macOS and Windows. Signed apt/dnf repository tooling, Homebrew, cargo-binstall and registry manifest sources are included. Distribution availability follows actual publication and catalog admission.

Compatibility

  • JSON results, exit statuses, key files and address pseudonyms are stable from 0.1.0. The command line, configuration and Server JSON logs may change in minor 0.x releases under the documented interface policy.
  • Windows does not yet support Don’t Fragment or DSCP marking. Other platform limits are documented in docs/platforms.md.
  • This is the first public release. There is no earlier public version to upgrade from. Existing development installations should review the key-file policy and configuration before switching.

About

udpstp measures network-path capacity, loss and delay. Use it to check access lines and circuits, compare paths, or collect JSON results for your measurement tools.

It implements the UDP Speed Test Protocol in RFC 9946 and the IP-Layer Capacity method in RFC 9097. It works with OB-UDPST key files and JSON output.

Who makes it

udpstp is created by Peder Bergan. You can also find him on GitHub, LinkedIn and Bluesky.

Also by Peder are Fastmash, Ripscope and Esdieff.

Licence

udpstp is free software, licensed under either of the Apache License, Version 2.0 or the MIT license, at your option.

This site

The documentation covers installation, running tests and understanding results. Every page is also available as Markdown at the same address with .md, and llms.txt lists them all. The site uses no cookies or analytics.

The headings use a subset of IBM Plex Sans Condensed Bold, renamed “udpstp Display” as its licence, the SIL Open Font License 1.1, requires of a modified version.