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
- Running a Test: options, JSON output and results.
- Hosting a Server: configuration and unattended operation.
- Share feedback: include your OS,
udpstp --version, command and warnings, keeping your key private.
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.
| Guide | For | Covers |
|---|---|---|
| Installing udpstp | Client users and Server administrators | Installing release binaries, platforms, upgrades and packages |
| Running a Test | Client users | Key files, Authentication Modes, Upstream and Downstream Tests, JSON results, Multi-Connection Tests and common problems |
| Hosting a Server | Server administrators | Configuration, keys, the firewall, limits, MaxHops, socket buffers, address privacy, debugging, running it as a service, and the log |
| udpstp on Windows | Windows users of either role | Private key files, the firewall, a trial Server and the Windows service |
| Operator features | Operators | Operator Keys, Fixed-Rate Tests, long Test Intervals, MaxHops and DSCP |
| Building udpstp | Contributors, and users who need a source build | Building 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
| Platform | Method | Availability |
|---|---|---|
| Linux | Archive installer, signed apt and dnf repositories | Available |
| Linux and macOS | Homebrew | Available |
| Windows | PowerShell installer and portable download | Available |
| Windows | WinGet | Awaiting catalog approval |
| All three | Cargo source build and cargo-binstall | Available |
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.
-
Save them in a key file. A name ending in
.tomlis a TOML Key Table:[[key]] admin_key_name = "customers-2026" key_id = 1 key_hex = "5b0c0e8d6f7a41c2b9d3e4f5061728394a5b6c7d8e9fa0b1c2d3e4f506172839"Give the key as
key_hex(hexadecimal) orkey_text(its UTF-8 bytes), not both. Any other file name is read as an OB-UDPSTudpst.keysfile, with one<keyId>,<key>line per key. -
Make the file private. On Linux and macOS:
chmod 600 keys.tomlOn 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 against | Use |
|---|---|
| a udpstp Server, or any Server that supports mode 2 | nothing 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
| Option | What it does | Default |
|---|---|---|
--direction upstream | Run an Upstream Test | downstream |
--test-interval SECONDS | The Test Interval, 5 to 60 s, or longer with an Operator Key that allows it (Operator features) | 10 |
--max-bandwidth MBPS | The most the Test is expected to carry. The Server searches no higher, and a Server with an Admitted Rate Cap may require it | none |
--start-row ROW | Start the search at this Sending Rate Table row. Row N offers N Mbps up to row 1000 | the Server’s choice |
--test-connections N | Run a Multi-Connection Test with N Test Connections | one per SERVER |
--algorithm c | Search with algorithm C (TR-471) instead of B | b |
--delay-var rtt | Judge by round-trip delay variation instead of one-way, which follows clock drift | one-way |
--random-payload | Fill Load PDUs with random bytes, so compression on the path can’t inflate the result | zeros |
--max-loss-ratio RATIO | The Performance Criterion: the most a Sub-Interval may lose and still count | 0.01 |
--refuse-coercion PARAMETER | End the Test if the Server changes this Activation parameter, named as in RFC 9946, such as testIntTime. Repeatable | accept the change |
--format json | Print JSON results (below) | text |
--max-hops HOPS | MaxHops: the TTL or hop limit of this end’s Test traffic | 64 |
--dscp DSCP | Mark the Test’s traffic with this DSCP. The Server may change it to 0 | 0 |
--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 Nruns 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 Nsets how many must start for the Test to go on. By default all of them must.--max-bandwidthis 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:
- the SERVER address and port;
- firewalls between you and the Server;
- the key and keyId;
- 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
| Status | Response | What to do |
|---|---|---|
| 102 | BADVER | Use a Client and Server that speak the same protocol version |
| 103, 111 | BADJS, BADTMTU | Match the Server’s --jumbo or --traditional-mtu setting |
| 109 | NOMAXBW | Set --max-bandwidth |
| 110 | CAPEXC | Retry later, or ask for less with --max-bandwidth |
| 113 | CONNFAIL | Retry 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 |
| 152 | BADPARAM | Often a Fixed-Rate Test without an Operator Key, or a Test that no longer fits the Server’s limits |
Other exit statuses
| Status | What to do |
|---|---|
| 50 | Fix the command line, or give a key file or --unauthenticated |
| 51 | Fix the key file. The message names the problem and the line |
| 75 | Check the SERVER name and your network |
| 107, 108, 157, 158 | Check the key, the keyId and both clocks |
| 200 | See No answer from the Server |
| 201 | Ask for a value the Server allows, or drop --refuse-coercion |
| 202 | Report it, with both ends’ versions |
| 203 | Use Servers with the same configuration for one Multi-Connection Test |
Exit statuses lists them all.
Warnings
| Warning | What to do |
|---|---|
| No Sub-Interval met the Performance Criterion | Compare 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 slower | Look 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 row | Test against a less loaded Server or at a lower row, or ask its administrator to check its log |
| The search never confirmed Congestion: the maxBandwidth | Raise --max-bandwidth, or leave it out |
| The search never confirmed Congestion: the top of the Sending Rate Table | Use more --test-connections |
| The search never confirmed Congestion: Hold Intervals | Test 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 rising | Use a longer --test-interval, or --algorithm c, which rises faster |
| The search never confirmed Congestion: a Load Sender stopped rising | If 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 set | Drop --traditional-mtu, or ask the Server’s administrator to lower its server_mtu |
| ICMP time-exceeded errors: –max-hops is too small for the path | Raise --max-hops above the path length |
| Status PDUs never arrived, or the peer had missed Status PDUs | Look for loss in the reverse direction |
| Status PDUs failed authentication and were ignored | Something on the path changed or injected them |
| Nothing was received for 1 s, or the Watchdog ended the Test | Check the path and the Server |
| Load PDUs without a delay sample: the wall clock stepped | Let 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.
-
Generate a random Shared Key of at least 32 bytes:
openssl rand -hex 32On Windows, use PowerShell.
-
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 = 3600Don’t use these example keys. Key files describes every field.
-
Make the file private. On Linux and macOS it must be mode
0600or stricter, owned by the account running udpstp or by root, in a folder only its owner can write:chmod 600 keys.tomlOn Windows, follow Key file permissions.
-
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
| Traffic | Port | Setting |
|---|---|---|
| Setup Requests and Responses | UDP 24601, the Standard Port | port |
| Each Test Connection’s traffic | One Ephemeral Port each, picked at random | ephemeral_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_portsrange. - 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
Recommended for a public Server
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.
-
Cap the admitted rate. Set
admitted_rate_cap, in Mbps for each direction, together withrequire_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 -
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 24Set a key’s own Quotas with
tests_per_quota_periodandbytes_per_quota_periodin its row. Operator Keys are exempt. Use is counted in memory, so a restart forgets it. -
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:
| Value | The log shows |
|---|---|
full | The address and port, such as 127.0.0.1:51801 |
truncate | The IPv4 /24 or IPv6 /48, without the port |
pseudonymize | anon- 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.
-
Print the effective configuration to start a file:
udpstp server --print-config > /etc/udpstp/server.tomlOr 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" -
Use absolute paths. A relative
key_fileis taken from the directory the Server starts in, not the file’s. -
Check the file, keys included:
udpstp server --config /etc/udpstp/server.toml --print-config -
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:
- 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. - 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.tomlwith 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
-
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). -
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:
-
Install the program where only administrators can change it. As administrator, copy
udpstp.exeintoC:\Program Files\udpstp. The firewall rule names this path and lets in whatever program is there. -
Add the firewall rule, as administrator, before
udpstp.exefirst runs from that path. Allow yourportand the wholeephemeral_portsrange, 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 AnyFor a Server that should answer only its own LAN, add
-RemoteAddress LocalSubnet. To remove the rule later, runRemove-NetFirewallRule -Group udpstp. -
Protect the key file as above, for your own account.
-
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.
-
As administrator, copy
udpstp.exetoC:\Program Files\udpstp\udpstp.exe. The service runs whatever program is at that path, so it must be where only administrators can change it. -
Put your Key Table in
C:\ProgramData\udpstp\keys.toml. -
From an elevated PowerShell, run the install script from the source’s
contrib\windowsfolder:powershell -ExecutionPolicy Bypass -File .\udpstp-server.ps1 -Install
The script:
- locks
C:\ProgramData\udpstpto Administrators and SYSTEM, with read access for the service and write access to itslogsfolder; - 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-serverwith no privileges and no credential to set or expire; - adds the firewall rule
udpstp-serverfor yourportandephemeral_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
| Allowance | Without an Operator Key | With one |
|---|---|---|
Fixed-Rate Tests (--fixed-rate) | Refused with BADPARAM | Allowed |
Test Interval (--test-interval) | At most 60 s; a longer request is coerced to 60 | Up to the key’s max_test_interval |
Starting row of a search (--start-row) | At most max_start_row, row 100 by default | Any row |
| Share of the Admitted Rate Cap one Test may state | At most max_test_share, 50% by default | Up to the whole cap |
| Quotas of Tests and load bytes | Counted | Exempt |
| DSCPs the Test may mark its traffic with | allowed_dscp, only 0 by default | operator_allowed_dscp, every DSCP but 48 and 56 (network control) by default |
| Time to infer Congestion (slowAdjThresh × trialInt) | Lowered to 500 ms at most | Kept 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,abortedorinterrupted. 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-privacyapplies 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-intervalsthere 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 10000the 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,clampandunrunnable). - 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
| Asset | Why it matters |
|---|---|
| Shared Keys and Operator Keys | Holding a Shared Key lets anyone start Tests. Holding an Operator Key also allows Fixed-Rate Tests. |
| Derived Keys | Anyone holding a Test Connection’s Derived Keys can forge its Control Phase PDUs, and its Status PDUs in mode 2. |
| The Server’s capacity | Link bandwidth, Ephemeral Ports, sockets, CPU and Admission state. All of these are finite. |
| Third parties’ networks | A 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 results | A 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. |
| Logs | Logs 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 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.
| Threat | Attacker | Defence | RFC basis | Implemented in |
|---|---|---|---|---|
| Forged Setup Request | Off-path | Mode 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, 0009 | crates/udpstp-core/src/server.rs (authenticate_setup), crates/udpstp-wire/src/auth.rs (verify_received) |
| Forged Setup or Activation Response | Off-path, on-path | The 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 0009 | crates/udpstp-core/src/client.rs (response_ignore_reason, handle_datagram) |
| Forged Activation Request | Off-path | The 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 0009 | crates/udpstp-core/src/server.rs (verify_activation, handle_activation_request) |
| Replay of a Setup or Activation Request after the time window | On-path | authUnixTime must be within ±5 s of the receiver’s clock. | §5.3.1; ADR 0009 | crates/udpstp-core/src/keys.rs (auth_time_acceptable) |
| Replay within the time window, including onto sibling Test Connections | On-path | A 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.1 | crates/udpstp-core/src/replay.rs, server.rs |
| Reflection: a spoofed source makes the Server send to a victim | Off-path | Requests 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 2 | crates/udpstp-core/src/server.rs, replay.rs, budget.rs, data_phase.rs (RECEIVE_TIMEOUT) |
| Amplification using multicast or broadcast addresses | Off-path | Setup 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). | §6 | crates/udpstp-core/src/io.rs (is_multicast_or_broadcast), server.rs, client.rs |
| Flood of unverified Setup Requests (CPU) | Off-path | A 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 4 | crates/udpstp-core/src/budget.rs, server.rs, keys.rs (KeyTable::accepting) |
| Exhaustion of Ephemeral Ports, sockets and bandwidth | Key holder, replayer | A 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-6 | crates/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 Connection | Off-path | In 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 rate | On-path | Mode 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.2 | crates/udpstp-core/src/data_phase.rs (handle_pdu, status_authentic, sanitize, police) |
| Forged Load PDUs hiding loss | Off-path with the Test Connection’s addresses, on-path | Load 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.2 | crates/udpstp-core/src/data_phase.rs (Lookback) |
| Forged STOP set or cleared | On-path | Clearing 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. | §11 | crates/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 Mode | On-path | authMode 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.2 | crates/udpstp-core/src/client.rs (response_ignore_reason), crates/udpstp-core/src/server.rs (handle_setup_request, verify_activation) |
| Consumers running Fixed-Rate Tests | Consumer | A Fixed-Rate request authenticated with anything other than an Operator Key is rejected with BADPARAM, and no load is sent. | §4.1 | crates/udpstp-core/src/server.rs (is_fixed_rate), proved by crates/udpstp-core/tests/fixed_rate.rs |
| Consumers running long or fast Tests | Consumer | The 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-6 | crates/udpstp-core/src/parameters.rs (clamp, Allowance), server.rs (max_start_row) |
| Key leak through the command line | Local user | There 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 permissions | Local user | Unix 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 0015 | crates/udpstp/src/keyfile.rs, crates/udpstp-key-permissions (Key handling) |
| Key leak through memory | Crash dump, swap | Shared 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 errors | Log readers | Key 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 skew | Misconfiguration | A ±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, 0010 | crates/udpstp-core/src/keys.rs (auth_time_acceptable) |
| Memory-unsafe parsing | Anyone | Protocol 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 flooding | Off-path | Nothing 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) andkey_id(the keyId, 0-255).- Optional
kdfandalg_id. Both must beHMAC-SHA-256if present. - Exactly one of
key_text(used as its UTF-8 bytes) orkey_hex. - Optional send and accept lifetimes in
YYYYMMDDHHMMSSZform. - Optional
operatorandmax_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-idmust 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
- Keep authentication on. Leave
allow_unauthenticatedoff (the default) on any Server that is reachable from networks you don’t control. - 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.
- 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. - 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_portsso that the rule stays small. Open the firewall has the details. - 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). - Synchronise clocks. Run NTP or similar. Clocks more than 5 s apart make every authenticated Test fail.
- 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. Everystatistics_interval(60 s by default) aStatisticsevent 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 aTestSummaryof 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_privacyshows 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 inaddress_privacy_key_file, so one Client keeps one pseudonym while the key is kept) or truncated to the IPv4 /24 or IPv6 /48. - Use the hardened unit or the container image.
contrib/systemd/udpstp-server.serviceruns 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.shchecks it withsystemd-analyze verifyandsecurity.contrib/container/Containerfilebuilds 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
| Interface | What is stable | Reference |
|---|---|---|
| Exit statuses | The rule behind them, and every value assigned | below |
JSON results (--format json) | Key names, types, enumerated values. The OB-UDPST part keeps OB-UDPST v9.1.0’s schema, except the recorded differences | JSON results |
| Key files | The TOML Key Table’s columns and format; OB-UDPST’s udpst.keys as OB-UDPST reads it; the pseudonym key file | below |
| stdout and stderr | Which output goes where | below |
| Address pseudonyms | The algorithm, so one key gives one Client the same pseudonym in every version | below |
Stable from 1.0
| Interface | What becomes stable | Reference |
|---|---|---|
| Command line and the Server’s TOML settings | Option and key names, value spellings, defaults | udpstp client --help, udpstp server --help, udpstp server --print-config; Server configuration |
The Server’s JSON log (log_format = "json") | Event names, field keys, enumerated values | Server 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’sMessage, the log’smessageanderrorfields. UseErrorStatus, a warning’sCode, the log’seventandreason. - The format of
udpstp --version. UDPSTP_FAULTSandUDPSTP_BLESS, which exist for udpstp’s own tests.UDPSTP_KERNEL_TX_TIMESTAMPS, an experimental setting for precision tests: set to1on Linux, a Load Sender asks the kernel to timestamp what it sends, and the Client’s JSON results addudpstp.KernelSendTimestamps(Samples,LagP50Us,LagP99Us,LagMaxUs: how much later than lpduTime the kernel sent them). Set tokernel-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_THREADSandUDPSTP_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.
| Information | Style |
|---|---|
| Errors and failed or interrupted outcomes | Bold red, with ✗ on the Test outcome |
| Warnings and qualified results | Bold amber, with ⚠ on the outcome and warnings heading |
| Completed Test without warnings | Bold green, with ✔ on the outcome |
| Section and help headings | Bold purple |
| Progress and INFO log labels | Blue, with ▶ on the start of a step |
| Table headings, timestamps and host metadata | Muted gray |
| Commands and option names | Bold, default foreground |
| Final Maximum IP-Layer Capacity | The Test outcome’s color: green for clean completion, amber for qualifications, red for failure or interruption |
| Other measurements, addresses and ordinary values | Default 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.
| Status | Meaning |
|---|---|
| 0 | The 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 |
| 50 | The configuration or command line is invalid |
| 51 | The key file can’t be read or is invalid |
| 75 | The Client or Server couldn’t start |
| 100 + code | The Server refused the Setup Request with that Command Response (102 to 113); 100 for a code RFC 9946 doesn’t define |
| 150 + code | The Server refused the Activation Request; 152 is BADPARAM, 150 an undefined code |
| 107, 108, 157, 158 | A Setup or Activation Response failed verification, or its time was outside the window |
| 200 | The Test didn’t complete |
| 201 | The Server coerced a parameter the Client was told to refuse |
| 202 | The Server set a parameter the Client can’t run with |
| 203 | A Multi-Connection Test’s Servers gave different parameters |
| 204 | The 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 |
| 255 | Any other error |
Running a Test says what to do about each.
stdout and stderr
| Program | stdout | stderr |
|---|---|---|
udpstp client --format json | The JSON results, and nothing else | Progress, warnings, errors |
udpstp client (text) | Progress and the text results | Errors, and warnings about this host |
udpstp server | The log | Errors and warnings at startup |
udpstp server with log_file | Nothing; the log goes to daily files named from log_file, with the startup’s errors and warnings as StartupError and StartupWarning | Errors and warnings at startup |
udpstp server --windows-service | None: a service has none; its log, startup included, goes to log_file | None |
udpstp server --print-config | The effective configuration, as TOML | Errors |
udpstp server --print-config --settings-only | Validated settings as TOML, without opening keys, logs or listeners | Settings 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>...
| Argument | Description |
|---|---|
<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 |
| Option | Description |
|---|---|
–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 |
–unauthenticated | Run 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-payload | Ask 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, –help | Print 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]
| Option | Description |
|---|---|
–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-config | Validate the settings and key file, print effective TOML, and exit |
–settings-only | With –print-config, preview settings without opening keys or logs |
–windows-service | Run as the Windows service the Service Control Manager started, which stops it, logging to log_file (Windows only) |
-h, –help | Print 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
| Setting | Default | Meaning |
|---|---|---|
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. |
port | 24601 | The UDP port the Server listens on for Setup Requests. |
allow_unauthenticated | false | Serve unauthenticated Tests too (not conformant to RFC 9946). |
checksum | false | Fill the header checksum and verify it when populated (RFC 9946 §5.6). |
jumbo | true | CHSR_JUMBO_STATUS: rows above 1 Gbps may use packets up to 9000 bytes (RFC 9946 §6.1). Clients must ask for the same. |
traditional_mtu | false | CHSR_TRADITIONAL_MTU: packets may be 1500 bytes, not 1250, up to 1 Gbps too. Clients must ask for the same. |
server_mtu | 1500 | The 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_source | 1 | How 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_connections | 256 | How many Test Connections activated Tests may reserve at once. |
test_connections_per_test | 24 | How many Test Connections one Test may have over its life. |
max_unactivated_test_connections | none | How 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_cap | none | The most maxBandwidth admitted at once in each direction, in Mbps; no cap when absent. |
max_test_share | 50 | The most of admitted_rate_cap, in percent, that one Test without an Operator Key may state, all its Test Connections together. |
require_max_bandwidth | false | Refuse Tests that state no maxBandwidth. |
troubleshooting | false | Answer authentication failures of Setup Requests with the reason instead of silence, to debug Clients. Off by default. |
unverified_setups_per_second | 20000 | How many Setup Requests a second may reach authentication; the rest are dropped silently. |
source_setup_burst | 48 | How many authentic Setup Requests one Source may send at once; at least test_connections_per_test. |
source_setups_per_second | 5 | How many more a Source may send each second after its burst. |
key_setup_burst | 1000 | How 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_second | 1000 | How 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_dscp | none | The DSCPs a Test with an Operator Key may mark its traffic with; absent, all but network control (48 and 56). |
max_start_row | 100 | The 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_period | 3600 | The Quota Period, in seconds: what Quotas are counted over. |
source_tests_per_quota_period | none | Each Source's Quota of Tests per Quota Period; absent, none. Operator Keys are exempt. |
source_bytes_per_quota_period | none | Each Source's Quota of load bytes per Quota Period, both directions; absent, none. Operator Keys are exempt. |
socket_buffer_size | 2097152 | Each Test Connection's receive and send buffer in its Data Phase, in bytes, within the OS's limits. |
dont_fragment | true | Set the Don't Fragment bit on test traffic (RFC 9946 §5: it SHOULD be set unless fragmentation is being measured). |
max_hops | none | MaxHops: 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_file | none | Where 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_file | none | The pseudonym key, as hexadecimal digits (16 bytes or more), for address_privacy = "pseudonymize". |
statistics_interval | 60 | How often the log reports the Server's counters, in seconds, up to a day; 0 never does. |
key_file | none | The 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
| Field | Type | What |
|---|---|---|
test_connection_id | integer | The Test Connection, the same in each of its events; left out for the Standard Port |
client | string | The Client’s address, as address_privacy shows it |
mc_ident | integer | The Test’s mcIdent. With client, it identifies the Test |
key_id | integer | The keyId of the Key Table row that authenticated the Test |
reason | string | Why, a name from a fixed set (below) |
count | integer | How many times it happened since the last such event |
error | string | The operating system’s error, in its words: free text |
direction | string | Upstream or Downstream |
parameter, requested, coerced | string, integer, integer | An Activation parameter by its RFC 9946 name, the value the Client asked for, and the value the Server gave |
row | integer | A Sending Rate Table row |
requested_bytes, receive_bytes, send_bytes | integer | Socket buffer sizes: asked for, and what the OS gave |
bursts, datagrams, bytes | integer | Bursts not sent, their Load PDUs, and their UDP payload bytes |
executor, engine_threads, grouping_us | string, integer, integer | How the Data Phase Engines run (DataPhaseEngines) |
fast_paths | string | The fast paths a Test Connection used (DataPhaseEnded) |
receive_buffer_overflows | integer | On 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_us | integer | Experimental send timing, in µs (DataPhaseEnded) |
Events
Starting and stopping
| Event | When | Fields |
|---|---|---|
DataPhaseEngines | At start: how the Data Phase Engines run (below) | executor, grouping_us, engine_threads, error |
Listening | The Server listens on an address | address |
NoIpv6Listener | The host has no IPv6, so the Server listens on IPv4 alone | error |
TroubleshootingModeOn | At start, and every minute while Troubleshooting Mode is on | |
StartupError | With log_file set: the Server couldn’t start | error, the message (free text); exit_status |
StartupWarning | With log_file set: a warning at startup | reason: DscpAskedAsZero, DscpCoercedToZero, DontFragmentUnsupported, AdmittedRateCapWithoutMaxBandwidth, NoTestAdmissible, TroubleshootingModeOn, SocketBuffersShort, SocketBuffersUnknown, MaxHopsNotSet or TestsPerSource |
Stopping | The Server was asked to stop. It ignores new Setup Requests, sends STOP to its active Test Connections and ends them | reason: SIGTERM, SIGINT or SIGHUP; on Windows CtrlC, CtrlBreak, CtrlClose or CtrlShutdown; as a Windows service ServiceStop or ServiceShutdown |
Setup and Activation
| Event | When | Fields |
|---|---|---|
TestConnectionAdmitted | A Setup Request was accepted | test_connection_id, client, mc_ident, and key_id in an authenticated mode |
SetupRequestRejected | A Setup Request was refused with a Setup Response | client, reason |
SetupRequestSilentlyRejected | A Setup Request was refused without a response (Silent Rejection) | client, reason |
SetupRequestIgnored | A Setup Request was ignored: DuplicateSetupRequest, a copy of one already answered; ArrivalAddressUnknown, which the Client retries; or ServerStopping | client, reason |
TestConnectionActivated | An Activation Request was accepted | test_connection_id, direction |
ActivationParameterCoerced | The Activation Response changed a parameter | test_connection_id, parameter, requested, coerced |
ActivationRequestRejected | An Activation Request was refused | test_connection_id, reason, and parameter for UnacceptableParameter |
ActivationRequestSilentlyRejected | An Activation Request was refused without a response | test_connection_id, reason |
ActivationWatchdogExpired | No valid Activation Request came within 3 s | test_connection_id |
UnactivatedTestConnectionEvicted | The Unactivated Test Connections filled their share, so the oldest of the Key Table row holding the most was closed to make room | test_connection_id |
The Data Phase
| Event | When | Fields |
|---|---|---|
FirstLoadPduReceived, FirstStatusPduReceived, StopReceived | The first Load PDU or Status PDU, and the STOP exchange | test_connection_id |
BurstsSkipped | This host fell behind sending load, so the Server, not the path, limited the Test | test_connection_id, bursts, datagrams, bytes |
BurstsWithheld | The send buffer was full | test_connection_id, bursts, datagrams, bytes |
InvalidLoadPdus, IgnoredLoadPdus, InvalidStatusPdus, StaleStatusPdus, StatusPdusFailedVerification, StatusPdusMissing, PeerMissedStatusPdus, PeerRxStopped, DelaySamplesDropped, IcmpErrors | PDUs ignored or missing, and ICMP errors, counted | test_connection_id, count |
PathMtuExceeded, HopLimitExceeded | Datagrams too large for the path with DF set, or expired on the way because max_hops is too small | test_connection_id, count |
LostStatusBackoff | No Status PDU came in time, so a Downstream Search Test backed off as for an Errored Interval | test_connection_id, row |
TrafficStopped | Nothing arrived for 1 s, so this end stopped sending | test_connection_id |
DataPhaseEnded | A Test Connection’s Data Phase ended (below) | test_connection_id, reason, fast_paths, receive_buffer_overflows, error |
TestConnectionReleased | A Test Connection ended and its Admission was released | test_connection_id |
TestSummary | A Test ended (below) |
Problems and counts
| Event | When | Fields |
|---|---|---|
SocketError, NoEphemeralPort | A socket failed, or no Ephemeral Port was free | test_connection_id (none for the Standard Port), error |
SocketBuffersShort | The OS held a Test Connection’s socket buffers below socket_buffer_size | test_connection_id, requested_bytes, receive_bytes, send_bytes |
DatagramsDropped | The Server’s Control Phase fell behind and discarded datagrams its socket readers had received. This is udpstp’s own queue, not a Receive Buffer Overflow | test_connection_id (none for the Standard Port), count |
Statistics, StatisticsRefusals | Every statistics_interval seconds, and once more when the Server stops (below) | |
Suppressed | Events of one kind beyond ten every 10 s, counted rather than logged | suppressed_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:
sendmmsgandUdpGsowhen the Server sent Load PDUs (not in an Upstream Test);recvmmsgandKernelRxTimestamps;epollwhen 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) andtest_connections;bytes_sentandbytes_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_datagramsandskipped_byteswhen it fell behind, andwithheld_bursts,withheld_datagramsandwithheld_byteswhen the send buffer was full; receive_buffer_overflows, on Linux, summed over its Test Connections;result:Completed;ServerStoppingfor a Test cut short when the Server stopped; how the first Test Connection that didn’t complete ended (aDataPhaseEndedreason,ActivationWatchdogExpired,UnactivatedTestConnectionEvicted, orActivationRequestRejectedwith itsreason); orNotActivated.
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) orDataPhaseEngineFailed. - 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, thenInputandOutputonce the Test was activated, thenErrorStatus,ErrorMessageandErrorMessage2. Output:BOMTimetoTimestampResolutionUsed, thenIncrementalResult,Summary,ModalSummary,AtMax,ModalResult,ECNBleachingDetected, thenEOMTimeandStatus.- Printed forms: delays in seconds from whole milliseconds (
0.011), Mbps and percentages with 2 decimals, ratios with 9, zero as0.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 onlyOutput’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:
| Value | Fields |
|---|---|
0 or 0.0 | CECountOfDelivered, CEPercentOfDelivered (every object), ECNBleachingDetected, Input.ECN, Input.CEThreshold |
"", 0.0 or 0 | Input.Interface, InterfaceEthMbps (every object), Input.InterfaceDeterminesMax |
| fixed | NumberFirstModeSuppSubIntervals 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-Connection | NumberOfConnections 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.
AtMaxandModalResultare 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 noAtMax(orModalResult), 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 asOutput.IncrementalResult, and its minimum RTT inudpstp.MinRTT.Outputstill has only its header and trailer. These partial results require the extension, which is on by default. SoftwareVersionis"udpstp <version>";ControlProtocolVersionis 20.- Delay fields of a Sub-Interval without samples are 0, as OB-UDPST writes, and the extension lists those Sub-Intervals;
Summaryis computed from real samples only. - A Sub-Interval never measured is left out of
IncrementalResultand listed in the extension;Intervalis its subIntSeqNo. IPLayerCapacityis 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:
| Key | What |
|---|---|
Warnings | The 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 |
EndReason | How 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 |
ControlPhaseFailure | Why 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 |
Coercions | Each 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, DataPhaseEngine | How 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 |
FastPaths | The 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 |
KernelSendTimestamps | Only 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) |
BurstLateness | In 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) |
PerformanceCriterion | MaxLossRatio |
CapacityModes | Each Capacity Mode’s Sub-Intervals, whether it has a valid result, its Maximum and Highest IP-Layer Capacity |
SubIntervals, MinRTT | Only 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, NoSamples | Sub-Intervals never measured, and those without delay or RTT samples |
IPLayerSenderBitRate | Per 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 |
TestConnections | A 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
Rolechanges; 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.
| ErrorStatus | Meaning |
|---|---|
| 0 | The Test completed, with the STOP exchange or when its Test Interval ran out |
| 49 | Ours, 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-6 | The 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 |
| 50 | The configuration or command line is invalid |
| 51 | The key file can’t be read or is invalid |
| 75 | The Client or Server couldn’t start |
| 100 + code | The 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 + code | The same for the Activation exchange: 152 for BADPARAM, 150 for an undefined code, 157 or 158 for a response that failed verification |
| 200 | The 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) |
| 201 | Ours: the Server coerced a parameter the Client was told to refuse |
| 202 | Ours: the Server set a parameter the Client can’t run with |
| 203 | Ours: the Servers gave a Multi-Connection Test’s Test Connections different parameters |
| 204 | Ours: 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 |
| 255 | Any 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
| Field | Meaning |
|---|---|
admin_key_name | A name for the row, unique in the file. Errors name the row by it, never by the key |
key_id | The keyId, 0 to 255, that Clients send |
key_hex or key_text | The key, as hexadecimal digits or as UTF-8 text. Exactly one |
send_lifetime_start, send_lifetime_end, accept_lifetime_start, accept_lifetime_end | Optional, YYYYMMDDHHMMSSZ in UTC |
kdf, alg_id | Optional. HMAC-SHA-256 if present |
operator, max_test_interval | An Operator Key, and its longest Test Interval in seconds (60 or more). See Operator features |
tests_per_quota_period, bytes_per_quota_period | This 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-idmust 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
0600or 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_textis an extension, andkey_hexaccepts either case (key-text-extension).- the KDF and AlgID columns are
HMAC-SHA-256, and the Operator marker andmax_test_intervalextend 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.
| Feature | Linux | macOS | Windows |
|---|---|---|---|
| Don’t Fragment (DF) on Test traffic | Set | Set | Not yet: both ends warn |
| A packet too large for the path | Lost, and reported as the Path MTU | Lost | Fragmented, since DF isn’t set |
| DSCP marking | Yes | Yes | No: the Client asks for DSCP 0, and the Server changes every Test’s DSCP to 0, each with a warning |
| Key file check | Mode 0600 or stricter, owned by the account or root | As Linux | Only the account, SYSTEM and Administrators may have access (details) |
| The Server’s Data Phase Engine | A pool of Engine Threads, one per CPU it may use, each serving many Test Connections | One Engine Thread per Test Connection | One Engine Thread per Test Connection |
| Fast paths | Batched sending (sendmmsg), UDP GSO, batched receiving (recvmmsg), kernel receive timestamps, waking on arrival | None | None |
| Receive Buffer Overflows counted | Yes | No | No |
| Socket buffer limit to raise | net.core.rmem_max, net.core.wmem_max | kern.ipc.maxsockbuf | — |
| An interrupted Client exits with | The signal’s status, such as 130 for Ctrl-C | The signal’s status | 204 |
| Unattended Server | systemd unit, container image | Your own service manager | Windows service |
| Firewall for a Server | A stateful firewall needs only the Standard Port | Not tested | Windows 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.
| Kind | Records |
|---|---|
| Deviations | 17 |
| Interpretations | 117 |
| OB-UDPST Differences | 31 |
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.
| State | Requirements |
|---|---|
| Tested | 3 |
| Operational | 3 |
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:
crates/udpstp/tests/interop.rs::unauthenticated_and_fixed_rate_tests_interoperatecrates/udpstp/tests/loopback.rs::fixed_rate_tests_complete_over_loopback
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaults
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/start_row_cap.rs::a_capped_search_still_climbs_past_the_cap
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.
| State | Requirements |
|---|---|
| Tested | 7 |
| Not applicable | 2 |
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp-core/src/results.rs::the_maximum_meets_the_criterion_and_the_highest_need_not
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.
| State | Requirements |
|---|---|
| Tested | 13 |
| Deviation | 1 |
| Operational | 1 |
| Not applicable | 2 |
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:
crates/udpstp/tests/netns.rs::a_fixed_rate_test_above_the_bottleneck_delivers_its_ratecrates/udpstp-core/tests/bottleneck.rs::a_fixed_rate_over_the_bottleneck_measures_its_capacity_and_losscrates/udpstp-core/tests/bottleneck.rs::a_fixed_rate_under_the_bottleneck_arrives_wholecrates/udpstp-core/tests/sub_interval_results.rs::a_downstream_client_reports_each_sub_interval_it_receivedcrates/udpstp-core/tests/sub_interval_results.rs::over_ipv6_the_headers_are_48_bytes
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.
| State | Requirements |
|---|---|
| Tested | 12 |
| Operational | 3 |
| Not applicable | 3 |
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:
crates/udpstp/src/results.rs::a_watchdog_ended_test_with_two_capacity_modescrates/udpstp/tests/netns.rs::downstream_and_upstream_tests_measure_the_bottleneckcrates/udpstp-core/src/results.rs::the_maximum_meets_the_criterion_and_the_highest_need_notcrates/udpstp-core/src/results.rs::a_sub_interval_with_nothing_received_meets_no_criterioncrates/udpstp-core/src/results.rs::no_sub_interval_meeting_the_criterion_means_no_maximum
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:
crates/udpstp-core/tests/delay_variation.rs::sub_intervals_collect_the_delay_samples_of_their_trial_intervalscrates/udpstp-core/tests/sub_interval_results.rs::duplicates_and_reordering_are_reported_apart_from_loss
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:
crates/udpstp/src/json.rs::a_bimodal_test_has_obudpst_keys_and_formscrates/udpstp/src/results.rs::a_watchdog_ended_test_with_two_capacity_modescrates/udpstp-core/src/results.rs::a_split_makes_two_capacity_modes
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.
| State | Requirements |
|---|---|
| Tested | 7 |
| Deviation | 1 |
| Not applicable | 2 |
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:
crates/udpstp/src/results.rs::an_upstream_testcrates/udpstp/tests/loopback.rs::fixed_rate_tests_complete_over_loopbackcrates/udpstp-core/src/data_phase.rs::sender_estimate_keeps_ordinary_gaps_duplicates_and_out_of_order_loadcrates/udpstp-core/tests/sender_estimate.rs::the_estimate_follows_what_the_server_sent_despite_losscrates/udpstp-core/tests/sub_interval_results.rs::an_upstream_client_reports_what_it_sent_per_st
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.
| State | Requirements |
|---|---|
| Tested | 37 |
| Deviation | 6 |
| Operational | 6 |
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:
crates/udpstp-core/src/sending_rate_table.rs::rows_offer_their_ratescrates/udpstp-core/tests/sending_rate_table.rs::row_n_is_n_mbps_up_to_1_gbps
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:
crates/udpstp-core/tests/client_parameters.rs::the_client_asks_for_the_start_row_chosencrates/udpstp-core/tests/sending_rate_table.rs::a_requested_starting_row_is_sent
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:
crates/udpstp-core/src/sending_rate_table.rs::rows_offer_their_ratescrates/udpstp-core/tests/sending_rate_table.rs::row_zero_sends_fixed_size_datagrams_near_0_1_mbps
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:
crates/udpstp-core/tests/plan_executor.rs::the_simulators_executor_meets_the_contractcrates/udpstp-core/tests/sending_rate_table.rs::every_row_up_to_1_gbps_fits_the_rfc_sizes_and_ob_udpst_client_limitscrates/udpstp-runtime/src/contract_tests.rs::the_data_phase_engine_meets_the_contractcrates/udpstp-runtime/src/engine/pool_tests.rs::the_pooled_executor_meets_the_contract
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/status_pdus.rs::status_pdus_follow_the_first_load_pdu_every_trial_interval
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:
crates/udpstp-core/tests/sequence_errors.rs::loss_counts_every_missing_load_pducrates/udpstp-core/tests/sequence_errors.rs::reordered_and_duplicated_load_pdus_are_counted
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:
crates/udpstp-core/tests/algorithm_b.rs::in_fast_mode_each_clean_interval_adds_high_speed_delta_rows
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:
crates/udpstp-core/tests/algorithm_b.rs::in_fast_mode_each_clean_interval_adds_high_speed_delta_rows
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::algorithm_b_never_leaves_single_step_mode
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:
crates/udpstp-core/tests/algorithm_b.rs::rows_stay_within_the_table_and_fast_mode_stops_at_1_gbpscrates/udpstp-core/tests/algorithm_b.rs::above_the_gigabit_row_every_step_is_one_row
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/interval_classification.rs::the_average_rounds_and_clean_is_strictly_below_the_low_thresholdcrates/udpstp-core/tests/interval_classification.rs::seq_err_thresh_is_always_applied
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::the_confirming_drop_stops_at_row_0_and_follows_the_row_it_starts_fromcrates/udpstp-core/tests/algorithm_b.rs::a_downstream_search_converges_near_the_bottleneck
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::intermittent_errored_intervals_do_not_confirm_congestion
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stop
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::traffic_that_comes_back_after_the_timeout_changes_nothing
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:
crates/udpstp-core/tests/lost_status_backoff.rs::lost_status_pdus_back_the_rate_off_and_confirm_congestioncrates/udpstp-core/tests/lost_status_backoff.rs::any_status_pdu_starts_the_backoff_over
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:
crates/udpstp-core/tests/lost_status_backoff.rs::lost_status_pdus_back_the_rate_off_and_confirm_congestioncrates/udpstp-core/tests/lost_status_backoff.rs::a_backoff_counts_as_an_errored_interval
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:
crates/udpstp-core/tests/lost_status_backoff.rs::lost_status_pdus_back_the_rate_off_and_confirm_congestioncrates/udpstp-core/tests/lost_status_backoff.rs::a_backoff_counts_as_an_errored_intervalcrates/udpstp-core/tests/lost_status_backoff.rs::backoffs_stop_at_row_0_and_two_in_a_row_leave_fast_mode_on
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:
crates/udpstp-core/tests/lost_status_backoff.rs::lost_status_pdus_back_the_rate_off_and_confirm_congestioncrates/udpstp-core/tests/lost_status_backoff.rs::a_backoff_counts_as_an_errored_interval
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:
crates/udpstp-core/tests/stale_status_pdus.rs::spdu_seq_err_reports_the_status_pdus_missing_before_the_latestcrates/udpstp-wire/tests/load_pdu.rs::header_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/status_pdu.rs::status_pdu_encodes_to_the_rfc_layout
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:
crates/udpstp-core/tests/tests_per_source.rs::a_second_test_from_the_same_source_is_refused_at_setup
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.
| State | Requirements |
|---|---|
| Tested | 9 |
| Deviation | 6 |
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validitycrates/udpstp/src/test_results.rs::human_context_preserves_private_identities_and_received_times
RFC9097-9-2
Deviation
The Maximum IP-Layer Capacity results SHOULD be reported in tabular format.
Recorded in compact-human-final-metrics.
Proving tests:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/json.rs::a_bimodal_test_has_obudpst_keys_and_formscrates/udpstp/src/results.rs::a_watchdog_ended_test_with_two_capacity_modescrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validitycrates/udpstp-core/src/results.rs::a_split_makes_two_capacity_modes
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validitycrates/udpstp/src/results.rs::maxima_keep_measured_rtt_minimum_without_variation
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.
| State | Requirements |
|---|---|
| Tested | 6 |
| Operational | 1 |
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:
crates/udpstp/tests/loopback.rs::test_connections_use_ports_from_the_configured_rangecrates/udpstp-core/tests/setup_exchange.rs::unauthenticated_setup_ends_in_ack_with_a_test_port_when_both_ends_opt_in
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:
crates/udpstp-core/tests/auth_mode_default.rs::a_server_without_mode_2_is_reported_and_the_client_does_not_fall_backcrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_key
RFC9097-10-4
Tested
Hosts MUST limit the number of simultaneous tests to avoid resource exhaustion and inaccurate results.
Note: Admission.
Proving tests:
crates/udpstp-core/src/admission.rs::a_cap_full_of_activated_tests_still_refusescrates/udpstp-core/tests/unactivated_share.rs::a_new_setup_evicts_the_oldest_unactivated_test_connection
RFC9097-10-5
Tested
Senders MUST be rate limited.
Note: Sending Rate Table; Admitted Rate Cap.
Proving tests:
crates/udpstp-core/tests/algorithm_b.rs::rows_stay_within_the_table_and_fast_mode_stops_at_1_gbpscrates/udpstp-core/tests/algorithm_capped.rs::fast_mode_stops_at_the_row_matching_max_bandwidth
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:
crates/udpstp-core/tests/algorithm_b.rs::in_fast_mode_each_clean_interval_adds_high_speed_delta_rowscrates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0
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.
| State | Requirements |
|---|---|
| Not applicable | 1 |
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.
| State | Requirements |
|---|---|
| Operational | 1 |
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.
| State | Requirements |
|---|---|
| Tested | 8 |
| Deviation | 1 |
| Operational | 6 |
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:
crates/udpstp-core/tests/multi_connection_aggregation.rs::the_summed_capacity_never_exceeds_the_shared_bottleneckcrates/udpstp-core/tests/multi_connection_aggregation.rs::upstream_sub_intervals_are_summed_by_number_and_their_offset_measuredcrates/udpstp-core/tests/multi_connection_failures.rs::the_test_goes_on_when_one_fails_and_enough_remaincrates/udpstp-core/tests/multi_connection_failures.rs::below_the_minimum_the_rest_are_stopped_and_releasedcrates/udpstp-core/tests/multi_connection_failures.rs::test_connections_that_fall_silent_together_stop_the_testcrates/udpstp-core/tests/multi_connection_failures.rs::failures_are_counted_until_too_few_remaincrates/udpstp-core/tests/multi_connection_failures.rs::failures_apart_are_isolated_onescrates/udpstp-core/tests/multi_connection_start.rs::too_few_setups_abort_the_test_before_any_activationcrates/udpstp-core/tests/multi_connection_start.rs::a_lower_minimum_goes_on_with_the_setups_that_succeededcrates/udpstp-core/tests/multi_connection_start.rs::an_unanswered_setup_is_given_up_once_enough_have_succeededcrates/udpstp-core/tests/multi_connection_start.rs::servers_that_set_different_parameters_abort_the_testcrates/udpstp-core/tests/multi_connection_start.rs::an_acceptance_that_arrives_after_the_abort_is_stopped_at_once
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:
crates/udpstp-core/tests/fixed_rate.rs::a_fixed_rate_test_is_refused_without_an_operator_keycrates/udpstp-core/tests/fixed_rate.rs::a_refused_downstream_fixed_rate_test_sends_no_load
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:
crates/udpstp-core/tests/algorithm_b.rs::a_fixed_rate_test_holds_its_rowcrates/udpstp-core/tests/fixed_rate.rs::an_operator_runs_a_fixed_rate_test_at_the_requested_row
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:
crates/udpstp-core/src/admission.rs::the_test_connections_of_one_test_are_one_testcrates/udpstp-core/tests/tests_per_source.rs::a_second_test_from_the_same_source_is_refused_at_setupcrates/udpstp-core/tests/tests_per_source.rs::of_two_tests_set_up_together_the_second_is_refused_at_activation
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.
| State | Requirements |
|---|---|
| Tested | 57 |
| Deviation | 3 |
| Operational | 8 |
| Not applicable | 5 |
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:
crates/udpstp/tests/netns.rs::downstream_and_upstream_tests_measure_the_bottleneckcrates/udpstp-runtime/src/ip_options.rs::test_traffic_carries_df_and_max_hops
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::the_confirming_drop_stops_at_row_0_and_follows_the_row_it_starts_from
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::intermittent_errored_intervals_do_not_confirm_congestion
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:
crates/udpstp-core/tests/algorithm_b.rs::the_accepted_parameters_drive_the_algorithmcrates/udpstp-core/tests/test_interval_limits.rs::slow_adj_thresh_times_trial_int_is_at_most_500_ms
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:
crates/udpstp-core/tests/interval_classification.rs::the_average_rounds_and_clean_is_strictly_below_the_low_thresholdcrates/udpstp-core/tests/interval_classification.rs::the_maximum_is_the_default_and_the_average_a_settingcrates/udpstp-core/tests/interval_classification.rs::no_delay_samples_is_never_cleancrates/udpstp-core/tests/interval_classification.rs::seq_err_thresh_is_always_appliedcrates/udpstp-core/tests/interval_classification.rs::an_upstream_server_classifies_the_status_pdus_it_sends
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:
crates/udpstp/tests/loopback.rs::the_client_reports_a_server_that_goes_awaycrates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stopcrates/udpstp-core/tests/timeouts.rs::load_pdus_failing_the_checksum_do_not_reset_the_watchdog
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stop
RFC9946-5.1-14
Tested
The default Load PDU timeout MUST be no more than 1 second.
Proving tests:
crates/udpstp/tests/loopback.rs::the_client_reports_a_server_that_goes_awaycrates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_s
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stop
RFC9946-5.1-17
Tested
The default Status PDU timeout MUST be no more than 1 second.
Proving tests:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stop
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:
crates/udpstp/tests/interop.rs::unauthenticated_and_fixed_rate_tests_interoperatecrates/udpstp/tests/loopback.rs::fixed_rate_tests_complete_over_loopback
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:
crates/udpstp/tests/interop.rs::our_client_completes_a_mode_1_downstream_test_against_obudpstcrates/udpstp/tests/loopback.rs::a_mode1_test_runs_with_a_toml_key_table_and_an_obudpst_key_filecrates/udpstp-core/tests/activation_mode1.rs::mode1_tests_are_activated_in_both_directionscrates/udpstp-core/tests/setup_mode1.rs::mode1_setup_between_our_cores_is_accepted
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::client_signs_its_activation_request_with_the_client_keycrates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_signed_with_the_client_key_and_answers_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::client_signs_its_setup_request_with_the_client_key_from_its_own_auth_unix_timecrates/udpstp-wire/tests/hmac_rfc4231.rs::hmac_sha256_matches_every_rfc_4231_test_casecrates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_reproduces_every_obudpst_pdu_from_its_fields
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:
crates/udpstp-core/tests/activation_mode1.rs::client_signs_its_activation_request_with_the_client_keycrates/udpstp-core/tests/setup_mode1.rs::client_signs_its_setup_request_with_the_client_key_from_its_own_auth_unix_time
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:
crates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/activation_mode1.rs::server_accepts_an_activation_time_5_s_offcrates/udpstp-core/tests/activation_mode1.rs::client_ignores_responses_that_fail_verificationcrates/udpstp-core/tests/setup_mode1.rs::clocks_five_seconds_apart_are_within_the_windowcrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_more_than_five_seconds_offcrates/udpstp-core/tests/setup_mode1.rs::setup_is_accepted_across_the_2106_wraparoundcrates/udpstp-core/tests/setup_mode1.rs::client_ignores_a_response_more_than_five_seconds_off
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:
crates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/activation_mode1.rs::server_verifies_the_digest_over_the_bytes_receivedcrates/udpstp-core/tests/activation_mode1.rs::client_ignores_responses_that_fail_verification
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:
crates/udpstp-core/tests/setup_mode1.rs::server_verifies_the_digest_over_the_bytes_receivedcrates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_whose_checksum_was_filled_after_the_digestcrates/udpstp-wire/tests/null_pdu.rs::a_signed_null_pdu_verifies_as_receivedcrates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_verifies_every_obudpst_digest
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:
crates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/key_table.rs::a_key_outside_its_accept_lifetime_is_an_authentication_failurecrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_signed_from_a_different_shared_keycrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_whose_key_id_it_does_not_havecrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_more_than_five_seconds_offcrates/udpstp-core/tests/troubleshooting_mode.rs::by_default_authentication_failures_are_silentcrates/udpstp-core/tests/troubleshooting_mode.rs::a_failed_activation_request_stays_silent_in_troubleshooting_mode
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::an_unknown_key_id_is_answered_with_authfailcrates/udpstp-core/tests/troubleshooting_mode.rs::a_time_outside_the_window_is_answered_with_a_signed_authtimecrates/udpstp-core/tests/troubleshooting_mode.rs::an_unauthenticated_request_is_answered_with_authreqcrates/udpstp-core/tests/troubleshooting_mode.rs::an_authenticated_request_to_a_server_without_keys_is_answered_with_authnccrates/udpstp-core/tests/troubleshooting_mode.rs::an_unsupported_mode_is_answered_with_authinv
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/null_request.rs::a_mode_1_null_request_is_signed_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::mode1_setup_between_our_cores_is_accepted
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:
crates/udpstp/tests/loopback.rs::a_mode2_test_completes_both_wayscrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_keycrates/udpstp-core/tests/mode2_status.rs::a_client_s_early_stop_is_authenticated
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:
crates/udpstp/tests/loopback.rs::a_mode2_test_completes_both_wayscrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_keycrates/udpstp-wire/tests/status_pdu.rs::a_signed_status_pdu_verifies_as_receivedcrates/udpstp-wire/tests/status_pdu.rs::verification_covers_every_byte_but_the_digest_and_checksum
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:
crates/udpstp-core/src/data_phase.rs::a_status_pdu_must_pass_every_mode_2_checkcrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_key
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:
crates/udpstp-core/tests/mode2_status.rs::in_mode_1_the_status_pdu_s_authentication_fields_are_zerocrates/udpstp-core/tests/status_pdus.rs::status_pdus_carry_the_auth_mode_and_zero_auth_fieldscrates/udpstp-core/tests/upstream.rs::the_server_sends_status_pdus_with_its_row
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:
crates/udpstp/tests/loopback.rs::a_mode2_test_completes_both_wayscrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_key
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:
crates/udpstp/tests/key_files.rs::the_server_loads_a_toml_key_tablecrates/udpstp-core/tests/key_table.rs::a_key_outside_its_accept_lifetime_is_an_authentication_failurecrates/udpstp-core/tests/key_table.rs::a_key_is_accepted_on_the_edges_of_its_accept_lifetimecrates/udpstp-core/tests/key_table.rs::a_shared_key_id_uses_the_row_valid_at_the_timecrates/udpstp-core/tests/key_table.rs::the_client_sends_with_the_newest_valid_key
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:
crates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_whose_key_id_it_does_not_have
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:
crates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_signed_with_the_client_key_and_answers_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::server_drops_a_request_whose_keys_come_from_another_auth_unix_timecrates/udpstp-core/tests/setup_mode1.rs::client_signs_its_setup_request_with_the_client_key_from_its_own_auth_unix_timecrates/udpstp-wire/tests/kdf_kat.rs::derived_keys_match_every_openssl_vector
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::client_signs_its_activation_request_with_the_client_keycrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_keycrates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_signed_with_the_client_key_and_answers_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::server_drops_a_request_signed_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::client_signs_its_setup_request_with_the_client_key_from_its_own_auth_unix_timecrates/udpstp-core/tests/setup_mode1.rs::client_ignores_a_response_signed_with_the_client_keycrates/udpstp-wire/tests/kdf_kat.rs::derived_keys_match_every_openssl_vectorcrates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_verifies_every_obudpst_digest
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::client_signs_its_activation_request_with_the_client_keycrates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_signed_with_the_client_key_and_answers_with_the_server_key
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:
crates/udpstp/tests/netns.rs::a_stateful_firewall_needs_only_the_standard_port_opencrates/udpstp-core/tests/null_request.rs::the_server_sends_a_null_request_after_the_setup_response
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:
crates/udpstp-wire/tests/load_pdu.rs::internet_checksum_matches_the_rfc_1071_examplecrates/udpstp-wire/tests/load_pdu.rs::internet_checksum_pads_an_odd_lengthcrates/udpstp-wire/tests/load_pdu.rs::internet_checksum_folds_every_carry
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:
crates/udpstp-core/tests/control_checksum.rs::every_control_pdu_carries_a_checksum_that_verifiescrates/udpstp-core/tests/downstream_load.rs::the_server_fills_the_header_checksum_when_configuredcrates/udpstp-core/tests/status_pdus.rs::status_pdus_carry_a_checksum_when_configuredcrates/udpstp-wire/tests/control_checksum.rs::a_zero_checksum_is_sent_as_ffffcrates/udpstp-wire/tests/load_pdu.rs::load_header_checksum_covers_the_header_onlycrates/udpstp-wire/tests/load_pdu.rs::a_zero_checksum_is_sent_as_all_onescrates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_reproduces_every_obudpst_pdu_from_its_fieldscrates/udpstp-wire/tests/status_pdu.rs::status_checksum_covers_the_whole_pducrates/udpstp-wire/tests/status_pdu.rs::a_zero_status_checksum_is_sent_as_all_ones
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:
crates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_request_is_dropped_before_authenticationcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_response_is_droppedcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_request_is_droppedcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_response_or_null_request_is_droppedcrates/udpstp-core/tests/control_checksum.rs::only_a_configured_receiver_verifies_and_only_a_populated_fieldcrates/udpstp-core/tests/downstream_load.rs::the_client_verifies_a_populated_checksum_when_configuredcrates/udpstp-core/tests/status_pdus.rs::the_load_sender_verifies_a_populated_status_checksumcrates/udpstp-wire/tests/load_pdu.rs::load_header_checksum_covers_the_header_onlycrates/udpstp-wire/tests/obudpst_control_kat.rs::the_checksum_build_s_pdus_carry_checksums_that_verifycrates/udpstp-wire/tests/status_pdu.rs::status_checksum_covers_the_whole_pdu
RFC9946-5.6-5
Tested
If PDU checksum validation fails, the PDU SHALL be discarded.
Proving tests:
crates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_request_is_dropped_before_authenticationcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_response_is_droppedcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_request_is_droppedcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_response_or_null_request_is_droppedcrates/udpstp-core/tests/downstream_load.rs::the_client_verifies_a_populated_checksum_when_configuredcrates/udpstp-core/tests/status_pdus.rs::the_load_sender_verifies_a_populated_status_checksum
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.
| State | Requirements |
|---|---|
| Tested | 58 |
| Deviation | 2 |
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:
crates/udpstp/tests/cli.rs::a_client_refuses_a_multicast_or_broadcast_servercrates/udpstp-core/tests/setup_addresses.rs::a_client_does_not_send_to_a_group_address
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:
crates/udpstp/tests/loopback.rs::a_setup_response_sent_to_a_broadcast_address_is_ignoredcrates/udpstp-core/tests/setup_addresses.rs::a_setup_request_from_a_group_address_is_dropped_silentlycrates/udpstp-core/tests/setup_addresses.rs::a_setup_request_to_a_group_address_is_dropped_silentlycrates/udpstp-core/tests/setup_addresses.rs::a_setup_response_to_a_group_address_is_droppedcrates/udpstp-core/tests/setup_addresses.rs::a_setup_response_from_a_group_address_is_ignored
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:
crates/udpstp/tests/netns.rs::a_test_over_ipv6_measures_the_bottleneckcrates/udpstp-core/tests/setup_addresses.rs::unicast_sources_are_answered
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:
crates/udpstp/tests/loopback.rs::a_client_completes_the_control_phase_over_ipv4_loopbackcrates/udpstp/tests/loopback.rs::the_client_uses_the_standard_port_by_defaultcrates/udpstp-core/tests/setup_exchange.rs::unauthenticated_setup_ends_in_ack_with_a_test_port_when_both_ends_opt_in
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:
crates/udpstp-core/tests/control_timers.rs::a_client_gives_up_3_s_after_its_setup_requestcrates/udpstp-core/tests/control_timers.rs::a_forged_setup_response_leaves_the_decision_to_the_timercrates/udpstp-core/tests/multi_connection_start.rs::an_unanswered_setup_is_given_up_once_enough_have_succeededcrates/udpstp-core/tests/setup_responses.rs::an_unverified_rejection_is_reported_but_the_timer_decides
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::server_verifies_the_digest_over_the_bytes_receivedcrates/udpstp-core/tests/setup_mode1.rs::server_verifies_the_digest_over_the_bytes_receivedcrates/udpstp-wire/tests/activation_pdu.rs::activation_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/activation_pdu.rs::reserved_fields_are_ignored_on_receiptcrates/udpstp-wire/tests/null_pdu.rs::reserved_fields_are_ignored_on_receiptcrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/setup_pdu.rs::reserved_field_is_ignored_on_receiptcrates/udpstp-wire/tests/status_pdu.rs::reserved_fields_are_ignored_on_decode
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:
crates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_reproduces_every_obudpst_pdu_from_its_fieldscrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_decodes_from_the_rfc_layout
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:
crates/udpstp-core/tests/algorithm_capped.rs::fast_mode_stops_at_the_row_matching_max_bandwidthcrates/udpstp-core/tests/algorithm_capped.rs::the_cap_masks_the_direction_bitcrates/udpstp-core/tests/algorithm_capped.rs::single_steps_stop_at_the_capcrates/udpstp-core/tests/algorithm_capped.rs::a_start_row_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_fixed_rate_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_cap_above_1_gbps_stops_at_the_row_within_it
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:
crates/udpstp/tests/interop.rs::our_client_completes_upstream_tests_against_obudpstcrates/udpstp-core/src/sending_rate_table.rs::packet_sizes_follow_the_modifiers_and_the_mtucrates/udpstp-core/tests/sending_rate_table.rs::every_row_up_to_1_gbps_fits_the_rfc_sizes_and_ob_udpst_client_limitscrates/udpstp-core/tests/table_variants.rs::srstruct_packet_sizes_follow_the_modifiers
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:
crates/udpstp-core/src/data_phase.rs::a_size_beyond_the_table_variant_is_refusedcrates/udpstp-core/src/sending_rate_table.rs::packet_sizes_follow_the_modifiers_and_the_mtucrates/udpstp-core/tests/table_variants.rs::srstruct_packet_sizes_follow_the_modifiers
RFC9946-6.1-10
Tested
This bit SHALL NOT be set by default.
Proving tests:
crates/udpstp/tests/cli.rs::the_table_variant_and_server_mtu_are_settingscrates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaults
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:
crates/udpstp-core/src/data_phase.rs::a_size_beyond_the_table_variant_is_refusedcrates/udpstp-core/tests/table_variants.rs::srstruct_packet_sizes_follow_the_modifiers
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:
crates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/setup_pdu.rs::reserved_field_is_ignored_on_receipt
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:
crates/udpstp-core/src/admission.rs::an_mc_index_not_below_mc_count_is_refusedcrates/udpstp-core/tests/multi_connection_start.rs::every_setup_goes_out_before_the_activations_which_go_out_together
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:
crates/udpstp-core/tests/mc_ident.rs::every_port_of_one_address_gives_its_own_non_zero_mc_identcrates/udpstp-core/tests/multi_connection_checks.rs::a_zero_mc_ident_is_refusedcrates/udpstp-core/tests/multi_connection_start.rs::every_setup_goes_out_before_the_activations_which_go_out_together
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:
crates/udpstp-core/tests/activation_exchange.rs::downstream_test_is_activated_on_the_test_portcrates/udpstp-core/tests/activation_exchange.rs::upstream_test_is_activatedcrates/udpstp-core/tests/null_request.rs::the_server_sends_a_null_request_after_the_setup_responsecrates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaults
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:
crates/udpstp-core/src/admission.rs::the_bandwidth_cap_holds_per_direction_with_usdir_maskedcrates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_bandwidth_cap_is_refused_with_capexccrates/udpstp-core/tests/test_share.rs::a_consumer_s_test_may_hold_half_the_cap
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:
crates/udpstp-core/src/admission.rs::the_bandwidth_cap_holds_per_direction_with_usdir_maskedcrates/udpstp-core/src/admission.rs::an_upstream_test_stating_no_max_bandwidth_may_leave_usdir_clearcrates/udpstp-core/tests/admission_limits.rs::the_client_states_its_max_bandwidth_and_sets_usdir_for_upstreamcrates/udpstp-core/tests/admission_limits.rs::an_activation_in_the_other_direction_than_the_setup_stated_is_refused
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:
crates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_request_is_dropped_before_authenticationcrates/udpstp-core/tests/null_request.rs::a_null_request_in_the_wrong_mode_is_droppedcrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_more_than_five_seconds_offcrates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_whose_checksum_was_filled_after_the_digestcrates/udpstp-core/tests/setup_mode1.rs::client_ignores_an_unauthenticated_response_to_a_mode1_requestcrates/udpstp-wire/tests/setup_pdu.rs::wrong_size_or_pdu_id_is_rejected
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:
crates/udpstp-core/tests/null_request.rs::a_forged_or_missing_null_request_changes_nothingcrates/udpstp-core/tests/null_request.rs::a_null_request_in_the_wrong_mode_is_droppedcrates/udpstp-core/tests/setup_rejections.rs::an_authentic_request_outside_the_time_window_gets_no_response
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:
crates/udpstp-core/tests/setup_rejections.rs::a_bad_protocol_version_is_rejected_with_the_expected_versioncrates/udpstp-wire/tests/setup_pdu.rs::wrong_size_or_pdu_id_is_rejected
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:
crates/udpstp/tests/loopback.rs::the_jumbo_setting_must_matchcrates/udpstp-core/tests/setup_rejections.rs::a_jumbo_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/setup_rejections.rs::a_traditional_mtu_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/setup_rejections.rs::an_unauthenticated_request_gets_unsigned_rejectionscrates/udpstp-core/tests/table_variants.rs::a_client_asking_for_the_server_s_modifiers_is_served
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:
crates/udpstp-core/tests/setup_rejections.rs::a_bad_protocol_version_is_rejected_with_the_expected_versioncrates/udpstp-core/tests/setup_rejections.rs::a_jumbo_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/source_budget.rs::a_request_over_its_sources_budget_gets_a_signed_connfailcrates/udpstp-core/tests/tests_per_source.rs::a_second_test_from_the_same_source_is_refused_at_setupcrates/udpstp-core/tests/troubleshooting_mode.rs::a_time_outside_the_window_is_answered_with_a_signed_authtime
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:
crates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_signed_from_a_different_shared_keycrates/udpstp-core/tests/setup_rejections.rs::a_request_whose_digest_fails_gets_no_responsecrates/udpstp-core/tests/troubleshooting_mode.rs::by_default_authentication_failures_are_silent
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:
crates/udpstp-core/tests/setup_exchange.rs::a_setup_request_of_the_wrong_size_gets_no_replycrates/udpstp-core/tests/setup_exchange.rs::a_setup_request_with_another_pdu_id_gets_no_reply
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:
crates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_decodes_from_the_rfc_layout
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::a_wrong_key_is_answered_with_authfailcrates/udpstp-core/tests/troubleshooting_mode.rs::an_unknown_key_id_is_answered_with_authfailcrates/udpstp-core/tests/troubleshooting_mode.rs::a_time_outside_the_window_is_answered_with_a_signed_authtimecrates/udpstp-core/tests/troubleshooting_mode.rs::an_unauthenticated_request_is_answered_with_authreqcrates/udpstp-core/tests/troubleshooting_mode.rs::an_authenticated_request_to_a_server_without_keys_is_answered_with_authnccrates/udpstp-core/tests/troubleshooting_mode.rs::an_unsupported_mode_is_answered_with_authinv
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:
crates/udpstp-core/tests/setup_mode1.rs::server_accepts_a_request_signed_with_the_client_key_and_answers_with_the_server_keycrates/udpstp-core/tests/setup_rejections.rs::a_mode_2_request_is_accepted_in_mode_2
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:
crates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_response_or_null_request_is_droppedcrates/udpstp-core/tests/null_request.rs::the_client_verifies_the_null_request_and_does_not_answercrates/udpstp-core/tests/null_request.rs::a_forged_or_missing_null_request_changes_nothingcrates/udpstp-core/tests/null_request.rs::a_null_request_in_the_wrong_mode_is_droppedcrates/udpstp-wire/tests/null_pdu.rs::wrong_size_or_pdu_id_is_rejected
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:
crates/udpstp-wire/tests/null_pdu.rs::null_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/null_pdu.rs::null_pdu_decodes_from_the_rfc_layout
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:
crates/udpstp-core/tests/null_request.rs::a_mode_1_null_request_is_signed_with_the_server_keycrates/udpstp-wire/tests/null_pdu.rs::a_signed_null_pdu_verifies_as_received
RFC9946-6.2.2-L4
Tested
Set to CHNR_CREQ_NULLREQ to indicate a Null Request message.
Note: Null Request cmdRequest field.
Proving tests:
crates/udpstp-core/tests/null_request.rs::the_server_sends_a_null_request_after_the_setup_responsecrates/udpstp-wire/tests/null_pdu.rs::null_pdu_encodes_to_the_rfc_layout
RFC9946-6.2.2-L5
Tested
Set to CHNR_CRSP_NONE.
Note: Null Request cmdResponse field.
Proving tests:
crates/udpstp-core/tests/null_request.rs::the_server_sends_a_null_request_after_the_setup_responsecrates/udpstp-wire/tests/null_pdu.rs::null_pdu_encodes_to_the_rfc_layout
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:
crates/udpstp-core/tests/control_checksum.rs::a_corrupted_setup_response_is_droppedcrates/udpstp-core/tests/control_timers.rs::a_forged_setup_response_leaves_the_decision_to_the_timercrates/udpstp-core/tests/setup_mode1.rs::client_accepts_a_response_signed_with_the_server_keycrates/udpstp-core/tests/setup_mode1.rs::client_ignores_a_response_signed_with_the_client_keycrates/udpstp-core/tests/setup_mode1.rs::client_ignores_a_response_more_than_five_seconds_offcrates/udpstp-core/tests/setup_mode1.rs::client_ignores_an_unauthenticated_response_to_a_mode1_requestcrates/udpstp-core/tests/setup_responses.rs::an_unverified_rejection_is_reported_but_the_timer_decides
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:
crates/udpstp-core/tests/setup_responses.rs::each_rejection_code_ends_the_setup_with_its_own_failure
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.
| State | Requirements |
|---|---|
| Tested | 44 |
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:
crates/udpstp/tests/loopback.rs::a_client_completes_the_control_phase_over_ipv4_loopbackcrates/udpstp-core/tests/activation_exchange.rs::downstream_test_is_activated_on_the_test_port
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:
crates/udpstp-core/tests/activation_coercion.rs::the_server_clears_a_requested_ecn_codepointcrates/udpstp-core/tests/activation_coercion.rs::the_client_sends_not_ect_whatever_the_server_answerscrates/udpstp-runtime/src/ip_options.rs::the_ds_field_reads_back
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/fixed_rate.rs::a_consumer_may_choose_the_starting_row_or_the_defaultcrates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaultscrates/udpstp-core/tests/sending_rate_table.rs::the_client_asks_for_the_row_in_its_activation_request
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/fixed_rate.rs::a_consumer_may_choose_the_starting_row_or_the_defaultcrates/udpstp-core/tests/sending_rate_table.rs::the_server_default_starts_at_row_zero
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:
crates/udpstp-wire/tests/activation_pdu.rs::activation_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/activation_pdu.rs::activation_pdu_decodes_from_the_rfc_layoutcrates/udpstp-wire/tests/obudpst_control_kat.rs::our_wire_layer_reproduces_every_obudpst_pdu_from_its_fields
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/control_checksum.rs::every_control_pdu_carries_a_checksum_that_verifies
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:
crates/udpstp-core/tests/activation_exchange.rs::downstream_test_is_activated_on_the_test_portcrates/udpstp-core/tests/activation_exchange.rs::upstream_test_is_activated
RFC9946-7.1-L3
Tested
Note that CHTA_CREQ_NONE remains unused.
Note: cmdRequest field.
Proving tests:
crates/udpstp-core/tests/parameter_ranges.rs::an_activation_request_with_cmd_request_none_is_refused
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:
crates/udpstp-core/tests/client_parameters.rs::the_client_asks_for_the_delay_variation_chosencrates/udpstp-core/tests/interval_classification.rs::the_average_rounds_and_clean_is_strictly_below_the_low_thresholdcrates/udpstp-core/tests/interval_classification.rs::without_one_way_delay_the_latest_rtt_sample_is_delay_varcrates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaults
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:
crates/udpstp-core/tests/plan_executor.rs::the_simulators_executor_meets_the_contractcrates/udpstp-core/tests/sending_rate_table.rs::every_row_up_to_1_gbps_fits_the_rfc_sizes_and_ob_udpst_client_limits
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:
crates/udpstp/tests/loopback.rs::batched_load_pdus_arrive_whole_in_order_and_at_their_sizescrates/udpstp-core/tests/downstream_load.rs::both_transmitters_and_the_add_on_are_sentcrates/udpstp-core/tests/upstream.rs::the_client_sends_the_servers_sending_rate_structure
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:
crates/udpstp/tests/loopback.rs::a_client_asks_for_a_dscp_the_server_allowscrates/udpstp/tests/loopback.rs::client_test_options_reach_the_servercrates/udpstp-core/tests/activation_coercion.rs::a_coerced_row_is_reported_at_both_endscrates/udpstp-core/tests/activation_coercion.rs::an_acceptable_request_is_echoed_without_coercionscrates/udpstp-core/tests/dscp.rs::an_allowed_dscp_marks_both_ends_traffic
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:
crates/udpstp-core/tests/activation_mode1.rs::server_answers_a_signed_activation_with_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/activation_mode1.rs::server_accepts_an_activation_time_5_s_offcrates/udpstp-core/tests/activation_mode1.rs::an_unauthenticated_test_connection_drops_a_mode1_activationcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_request_is_droppedcrates/udpstp-core/tests/troubleshooting_mode.rs::a_failed_activation_request_stays_silent_in_troubleshooting_modecrates/udpstp-wire/tests/activation_pdu.rs::wrong_size_or_pdu_id_is_rejected
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:
crates/udpstp-core/tests/activation_coercion.rs::a_coerced_row_is_reported_at_both_endscrates/udpstp-core/tests/activation_coercion.rs::an_acceptable_request_is_echoed_without_coercionscrates/udpstp-core/tests/activation_coercion.rs::an_unknown_rate_adjustment_algorithm_is_rejected_with_badparam
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:
crates/udpstp-core/tests/fixed_rate.rs::a_consumer_may_choose_the_starting_row_or_the_defaultcrates/udpstp-core/tests/start_row_cap.rs::an_operator_key_is_exempt
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:
crates/udpstp-core/tests/start_row_cap.rs::a_consumer_s_start_row_is_coerced_to_the_capcrates/udpstp-core/tests/start_row_cap.rs::by_default_a_search_starts_at_row_100_at_most
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:
crates/udpstp-core/tests/activation_coercion.rs::a_coerced_row_is_reported_at_both_endscrates/udpstp-core/tests/fixed_rate.rs::a_row_beyond_the_table_is_coerced_to_the_last_rowcrates/udpstp-core/tests/start_row_cap.rs::a_consumer_s_start_row_is_coerced_to_the_capcrates/udpstp-core/tests/start_row_cap.rs::by_default_a_search_starts_at_row_100_at_most
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:
crates/udpstp-core/tests/activation_coercion.rs::a_coerced_row_is_reported_at_both_endscrates/udpstp-core/tests/fixed_rate.rs::a_row_beyond_the_table_is_coerced_to_the_last_rowcrates/udpstp-core/tests/parameter_ranges.rs::out_of_range_values_are_clamped_to_the_nearest_limit
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:
crates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/troubleshooting_mode.rs::a_failed_activation_request_stays_silent_in_troubleshooting_mode
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:
crates/udpstp/tests/loopback.rs::a_client_asks_for_a_dscp_the_server_allowscrates/udpstp-core/tests/activation_coercion.rs::the_server_clears_a_requested_ecn_codepointcrates/udpstp-core/tests/dscp.rs::a_server_allowing_only_the_default_coerces_a_priority_dscp_to_0crates/udpstp-core/tests/dscp.rs::a_server_that_cant_mark_allows_only_0_even_to_operatorscrates/udpstp-core/tests/interval_classification.rs::the_accepted_threshold_classifies_not_the_requested_onecrates/udpstp-core/tests/parameter_ranges.rs::out_of_range_values_are_clamped_to_the_nearest_limitcrates/udpstp-core/tests/parameter_ranges.rs::a_low_threshold_above_the_upper_one_is_lowered_to_itcrates/udpstp-core/tests/parameter_ranges.rs::a_sub_interval_longer_than_the_test_is_shortened
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:
crates/udpstp/tests/interop.rs::unauthenticated_and_fixed_rate_tests_interoperatecrates/udpstp-core/tests/activation_coercion.rs::an_unknown_rate_adjustment_algorithm_is_rejected_with_badparamcrates/udpstp-core/tests/fixed_rate.rs::a_fixed_rate_test_is_refused_without_an_operator_key
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:
crates/udpstp-core/tests/activation_mode1.rs::server_silently_drops_activations_that_fail_verificationcrates/udpstp-core/tests/activation_mode1.rs::server_signs_badparam_rejections
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::at_most_10_failures_a_second_are_answeredcrates/udpstp-core/tests/troubleshooting_mode.rs::a_replay_stays_silentcrates/udpstp-core/tests/troubleshooting_mode.rs::a_failed_activation_request_stays_silent_in_troubleshooting_mode
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:
crates/udpstp-core/tests/activation_exchange.rs::upstream_test_is_activatedcrates/udpstp-core/tests/fixed_rate.rs::an_operator_runs_a_fixed_rate_test_at_the_requested_rowcrates/udpstp-core/tests/fixed_rate.rs::a_consumer_may_choose_the_starting_row_or_the_defaultcrates/udpstp-core/tests/sending_rate_table.rs::the_server_default_starts_at_row_zerocrates/udpstp-core/tests/sending_rate_table.rs::a_requested_starting_row_is_sentcrates/udpstp-core/tests/sending_rate_table.rs::a_requested_fixed_rate_row_is_sentcrates/udpstp-core/tests/sending_rate_table.rs::a_low_row_is_an_add_on_alone
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:
crates/udpstp-core/tests/algorithm_b.rs::the_default_search_starts_at_row_0crates/udpstp-core/tests/algorithm_b.rs::a_fixed_rate_test_holds_its_rowcrates/udpstp-core/tests/downstream_load.rs::a_fixed_row_is_sent_as_load_pduscrates/udpstp-core/tests/downstream_load.rs::both_transmitters_and_the_add_on_are_sent
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:
crates/udpstp-core/tests/activation_mode1.rs::client_accepts_a_response_signed_with_the_server_keycrates/udpstp-core/tests/activation_mode1.rs::client_ignores_responses_that_fail_verificationcrates/udpstp-core/tests/control_checksum.rs::a_corrupted_activation_response_or_null_request_is_dropped
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:
crates/udpstp-core/tests/activation_coercion.rs::the_client_reports_each_parameter_the_server_coercedcrates/udpstp-core/tests/activation_coercion.rs::the_client_reports_a_coercion_of_every_coercible_parametercrates/udpstp-core/tests/activation_coercion.rs::the_client_reports_changed_modifier_bitscrates/udpstp-core/tests/activation_coercion.rs::the_client_accepts_coercions_it_did_not_refusecrates/udpstp-core/tests/dscp.rs::a_server_allowing_only_the_default_coerces_a_priority_dscp_to_0crates/udpstp-core/tests/test_interval_limits.rs::the_client_runs_a_long_test_its_operator_key_allows
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:
crates/udpstp-core/tests/activation_exchange.rs::downstream_test_is_activated_on_the_test_portcrates/udpstp-core/tests/activation_exchange.rs::upstream_test_is_activatedcrates/udpstp-core/tests/activation_mode1.rs::mode1_tests_are_activated_in_both_directionscrates/udpstp-core/tests/upstream.rs::the_client_sends_the_servers_sending_rate_structure
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:
crates/udpstp/tests/loopback.rs::the_client_reports_a_server_that_goes_awaycrates/udpstp-core/tests/control_timers.rs::an_activated_client_does_not_time_out
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:
crates/udpstp/tests/loopback.rs::client_test_options_reach_the_servercrates/udpstp-core/tests/activation_coercion.rs::the_client_ends_the_test_when_a_coercion_is_unacceptablecrates/udpstp-core/tests/activation_coercion.rs::the_first_refused_coercion_in_field_order_is_reportedcrates/udpstp-core/tests/activation_coercion.rs::the_client_reports_changed_modifier_bitscrates/udpstp-core/tests/activation_coercion.rs::the_client_refuses_a_dscp_it_did_not_ask_forcrates/udpstp-core/tests/multi_connection_start.rs::an_acceptance_that_arrives_after_the_abort_is_stopped_at_oncecrates/udpstp-core/tests/parameter_ranges.rs::the_client_ends_a_test_it_cannot_runcrates/udpstp-core/tests/stop_exchange.rs::a_client_that_refuses_a_coercion_stops_the_server_at_oncecrates/udpstp-core/tests/test_interval_limits.rs::the_client_refuses_a_longer_test_than_it_asked_for
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.
| State | Requirements |
|---|---|
| Tested | 55 |
| Deviation | 2 |
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:
crates/udpstp-core/tests/algorithm_b.rs::an_upstream_search_converges_near_the_bottleneckcrates/udpstp-core/tests/upstream.rs::the_server_sends_status_pdus_with_its_rowcrates/udpstp-core/tests/upstream.rs::the_client_follows_the_srstruct_of_later_status_pdus
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:
crates/udpstp-core/tests/downstream_load.rs::the_client_ignores_invalid_load_pduscrates/udpstp-core/tests/downstream_load.rs::the_client_receives_a_valid_load_pdu_after_invalid_onescrates/udpstp-core/tests/downstream_load.rs::the_client_verifies_a_populated_checksum_when_configuredcrates/udpstp-wire/tests/load_pdu.rs::decoding_rejects_short_datagrams_and_other_pdu_ids
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:
crates/udpstp-core/tests/downstream_load.rs::the_client_ignores_invalid_load_pduscrates/udpstp-core/tests/downstream_load.rs::the_client_receives_a_valid_load_pdu_after_invalid_onescrates/udpstp-wire/tests/load_pdu.rs::decoding_rejects_short_datagrams_and_other_pdu_ids
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:
crates/udpstp-core/tests/timeouts.rs::traffic_that_comes_back_after_the_timeout_changes_nothingcrates/udpstp-core/tests/timeouts.rs::load_pdus_failing_the_checksum_do_not_reset_the_watchdog
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:
crates/udpstp-core/tests/upstream.rs::the_client_sends_the_servers_sending_rate_structurecrates/udpstp-core/tests/upstream.rs::the_client_follows_the_srstruct_of_later_status_pduscrates/udpstp-core/tests/upstream.rs::a_new_srstruct_can_swap_transmitters
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:
crates/udpstp-core/tests/algorithm_b.rs::the_accepted_parameters_drive_the_algorithmcrates/udpstp-core/tests/parameter_ranges.rs::the_client_asks_for_the_defaults
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:
crates/udpstp/tests/loopback.rs::a_client_can_ask_for_algorithm_ccrates/udpstp-core/tests/activation_coercion.rs::algorithm_c_is_acceptedcrates/udpstp-core/tests/algorithm_c.rs::a_search_with_algorithm_c_converges_near_the_bottleneck
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:
crates/udpstp-wire/tests/load_pdu.rs::header_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/load_pdu.rs::header_decodes_from_the_rfc_layout_with_any_payload_after_it
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:
crates/udpstp-core/tests/timeouts.rs::rx_stopped_clears_when_traffic_comes_back_in_timecrates/udpstp-core/tests/timeouts.rs::the_load_sender_keeps_sending_when_status_pdus_come_back_in_time
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:
crates/udpstp-wire/tests/load_pdu.rs::header_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/load_pdu.rs::header_decodes_from_the_rfc_layout_with_any_payload_after_it
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::intermittent_errored_intervals_do_not_confirm_congestioncrates/udpstp-core/tests/interval_classification.rs::the_average_rounds_and_clean_is_strictly_below_the_low_threshold
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::intermittent_errored_intervals_do_not_confirm_congestion
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stopcrates/udpstp-core/tests/timeouts.rs::rx_stopped_is_clear_while_traffic_flowscrates/udpstp-core/tests/timeouts.rs::a_load_pdu_with_undefined_flags_is_invalid
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_s
RFC9946-8.1-L6
Tested
A four-octet field indicating the Load PDU sequence number (starting at 1).
Note: lpduSeqNo field.
Proving tests:
crates/udpstp/tests/loopback.rs::batched_load_pdus_arrive_whole_in_order_and_at_their_sizescrates/udpstp-core/tests/downstream_load.rs::a_fixed_row_is_sent_as_load_pduscrates/udpstp-core/tests/sequence_errors.rs::loss_counts_every_missing_load_pducrates/udpstp-core/tests/sequence_errors.rs::a_first_load_pdu_numbered_0_counts_no_losscrates/udpstp-core/tests/sequence_errors.rs::a_forged_first_load_pdu_is_ignored
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:
crates/udpstp/tests/loopback.rs::batched_load_pdus_arrive_whole_in_order_and_at_their_sizescrates/udpstp-core/tests/status_pdus.rs::bytes_are_counted_from_the_datagram_not_its_udp_payload_fieldcrates/udpstp-core/tests/sub_interval_results.rs::a_forged_udp_payload_field_doesnt_inflate_the_capacitycrates/udpstp-runtime/src/socket/batch_read.rs::batched_datagrams_arrive_whole_and_in_order
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:
crates/udpstp-core/tests/stale_status_pdus.rs::spdu_seq_err_reports_the_status_pdus_missing_before_the_latestcrates/udpstp-core/tests/status_loss.rs::a_load_sender_counts_the_status_pdus_it_never_receivedcrates/udpstp-core/tests/status_loss.rs::a_load_receiver_counts_load_pdus_reporting_missed_status_pdus
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_is_the_time_since_the_status_pdu_arrived
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:
crates/udpstp-core/tests/downstream_load.rs::lpdu_time_is_the_send_time_and_ignores_wall_clock_steps
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_is_the_time_since_the_status_pdu_arrivedcrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_counts_from_the_kernel_receive_time
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rtt
RFC9946-8.1-L13
Tested
The checksum does not cover the payload content.
Note: checkSum field of the Load PDU.
Proving tests:
crates/udpstp-core/tests/downstream_load.rs::the_server_fills_the_header_checksum_when_configuredcrates/udpstp-wire/tests/load_pdu.rs::load_header_checksum_covers_the_header_only
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:
crates/udpstp-core/tests/downstream_load.rs::the_server_fills_the_header_checksum_when_configuredcrates/udpstp-wire/tests/load_pdu.rs::load_header_checksum_covers_the_header_only
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:
crates/udpstp-core/tests/downstream_load.rs::payload_content_is_zeros_by_defaultcrates/udpstp-core/tests/downstream_load.rs::payload_content_is_random_when_asked_for
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:
crates/udpstp-core/src/data_phase.rs::sender_estimate_work_is_bounded_for_large_forward_timestampscrates/udpstp-core/tests/status_pdus.rs::status_pdus_follow_the_first_load_pdu_every_trial_intervalcrates/udpstp-core/tests/status_pdus.rs::no_status_pdu_is_sent_before_a_load_pdu_arrivescrates/udpstp-core/tests/upstream.rs::the_server_sends_status_pdus_with_its_row
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:
crates/udpstp-core/tests/status_pdus.rs::the_load_sender_rejects_status_pdus_of_other_sizescrates/udpstp-core/tests/status_pdus.rs::the_load_sender_verifies_a_populated_status_checksumcrates/udpstp-wire/tests/status_pdu.rs::decoding_rejects_other_sizes_and_pdu_ids
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:
crates/udpstp-wire/tests/status_pdu.rs::status_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/status_pdu.rs::status_pdu_decodes_from_the_rfc_layout
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:
crates/udpstp-core/tests/delay_variation.rs::sub_intervals_collect_the_delay_samples_of_their_trial_intervalscrates/udpstp-core/tests/sequence_errors.rs::sub_intervals_count_sequence_errors_toocrates/udpstp-core/tests/status_pdus.rs::each_status_pdu_counts_its_trial_intervalcrates/udpstp-core/tests/status_pdus.rs::sub_intervals_are_counted_from_the_first_status_pdu
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_var_sample_has_no_value_without_a_new_sample
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:
crates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pducrates/udpstp-core/tests/status_pdus.rs::status_pdus_follow_the_first_load_pdu_every_trial_interval
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:
crates/udpstp-wire/tests/status_pdu.rs::status_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/status_pdu.rs::status_pdu_decodes_from_the_rfc_layout
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:
crates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_keycrates/udpstp-core/tests/status_pdus.rs::status_pdus_carry_a_checksum_when_configured
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:
crates/udpstp-core/src/data_phase.rs::sender_estimate_keeps_ordinary_gaps_duplicates_and_out_of_order_loadcrates/udpstp-core/tests/sequence_errors.rs::the_rfc_example_counts_four_out_of_ordercrates/udpstp-core/tests/sequence_errors.rs::a_repeated_load_pdu_is_a_duplicatecrates/udpstp-core/tests/sequence_errors.rs::reordered_and_duplicated_load_pdus_are_counted
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:
crates/udpstp-core/tests/sequence_errors.rs::a_repeated_load_pdu_is_a_duplicatecrates/udpstp-core/tests/sequence_errors.rs::the_lookback_holds_32_load_pdus
RFC9946-8.2-L6
Tested
Each Load PDU sequence number is checked against this lookback.
Note: seqErrOoo and seqErrDup.
Proving tests:
crates/udpstp-core/tests/sequence_errors.rs::a_repeated_load_pdu_is_a_duplicatecrates/udpstp-core/tests/sequence_errors.rs::a_late_load_pdu_enters_the_lookback
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:
crates/udpstp-core/src/data_phase.rs::sender_estimate_keeps_ordinary_gaps_duplicates_and_out_of_order_loadcrates/udpstp-core/tests/sequence_errors.rs::the_rfc_example_counts_four_out_of_order
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:
crates/udpstp-core/tests/bottleneck.rs::a_full_buffer_shows_as_its_drain_time_in_delay_variationcrates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pdu
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:
crates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pducrates/udpstp-core/tests/delay_variation.rs::clock_offset_and_drift_show_in_clock_delta_min_only
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rtt
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_var_sample_has_no_value_without_a_new_sample
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:
crates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pducrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rtt
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.
| State | Requirements |
|---|---|
| Tested | 12 |
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:
crates/udpstp-core/tests/stop_exchange.rs::the_test_stops_gracefully_when_the_servers_test_interval_endscrates/udpstp-core/tests/upstream.rs::an_upstream_test_stops_gracefully
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:
crates/udpstp-core/tests/stop_exchange.rs::the_test_stops_gracefully_when_the_servers_test_interval_endscrates/udpstp-core/tests/stop_exchange.rs::only_stop2_stops_the_client_and_later_load_pdus_are_ignoredcrates/udpstp-core/tests/upstream.rs::an_upstream_test_stops_gracefully
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:
crates/udpstp-core/tests/stop_exchange.rs::the_test_stops_gracefully_when_the_servers_test_interval_endscrates/udpstp-core/tests/stop_exchange.rs::only_stop2_stops_the_client_and_later_load_pdus_are_ignoredcrates/udpstp-core/tests/upstream.rs::an_upstream_test_stops_gracefullycrates/udpstp-core/tests/upstream.rs::a_stop_status_pdu_does_not_change_the_ratecrates/udpstp-core/tests/upstream.rs::a_client_with_nothing_to_send_still_confirms_a_stop
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:
crates/udpstp-core/tests/stop_exchange.rs::the_test_stops_gracefully_when_the_servers_test_interval_endscrates/udpstp-core/tests/upstream.rs::an_upstream_test_stops_gracefully
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stop
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.
| State | Requirements |
|---|---|
| Tested | 1 |
| Deviation | 1 |
| Operational | 3 |
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:
crates/udpstp-core/tests/tests_per_source.rs::a_second_test_from_the_same_source_is_refused_at_setup
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.
| State | Requirements |
|---|---|
| Tested | 6 |
| Deviation | 1 |
| Operational | 1 |
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:
crates/udpstp/tests/loopback.rs::test_connections_use_ports_from_the_configured_rangecrates/udpstp-core/tests/setup_exchange.rs::unauthenticated_setup_ends_in_ack_with_a_test_port_when_both_ends_opt_in
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:
crates/udpstp-core/tests/auth_mode_default.rs::a_server_without_mode_2_is_reported_and_the_client_does_not_fall_backcrates/udpstp-core/tests/mode2_status.rs::each_status_pdu_is_signed_with_its_sender_s_key
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:
crates/udpstp-core/src/admission.rs::a_cap_full_of_activated_tests_still_refusescrates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_per_test_cap_is_refused_with_connfailcrates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_global_cap_is_refused_with_connfailcrates/udpstp-core/tests/unactivated_share.rs::a_new_setup_evicts_the_oldest_unactivated_test_connection
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:
crates/udpstp-core/tests/algorithm_capped.rs::a_cap_above_1_gbps_stops_at_the_row_within_itcrates/udpstp-core/tests/upstream.rs::a_client_keeps_to_its_max_bandwidth_against_a_server_that_assigns_more
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:
crates/udpstp-core/src/admission.rs::the_quota_refills_over_the_quota_periodcrates/udpstp-core/tests/quotas.rs::a_source_past_its_quota_of_tests_is_refused_with_connfailcrates/udpstp-core/tests/quotas.rs::a_source_past_its_quota_of_bytes_is_refusedcrates/udpstp-core/tests/quotas.rs::tests_set_up_together_past_the_quota_are_refused_at_activationcrates/udpstp-core/tests/quotas.rs::a_key_past_its_quota_is_refusedcrates/udpstp-core/tests/test_interval_limits.rs::an_ordinary_key_gets_at_most_60_s
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:
crates/udpstp-core/tests/algorithm_capped.rs::fast_mode_stops_at_the_row_matching_max_bandwidthcrates/udpstp-core/tests/algorithm_capped.rs::the_cap_masks_the_direction_bitcrates/udpstp-core/tests/algorithm_capped.rs::single_steps_stop_at_the_capcrates/udpstp-core/tests/algorithm_capped.rs::a_start_row_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_fixed_rate_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_cap_above_1_gbps_stops_at_the_row_within_itcrates/udpstp-core/tests/test_share.rs::a_consumer_s_test_may_hold_half_the_cap
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.
| State | Requirements |
|---|---|
| Tested | 13 |
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:
crates/udpstp/tests/cli.rs::the_server_defaults_apply_without_a_filecrates/udpstp/tests/loopback.rs::the_client_uses_the_standard_port_by_default
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:
crates/udpstp-wire/tests/load_pdu.rs::header_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layoutcrates/udpstp-wire/tests/status_pdu.rs::status_pdu_encodes_to_the_rfc_layout
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:
crates/udpstp-core/tests/setup_rejections.rs::a_bad_protocol_version_is_rejected_with_the_expected_versioncrates/udpstp-wire/tests/setup_pdu.rs::setup_pdu_encodes_to_the_rfc_layout
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:
crates/udpstp-core/tests/setup_rejections.rs::a_jumbo_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/table_variants.rs::a_client_asking_for_the_server_s_modifiers_is_served
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:
crates/udpstp-core/tests/setup_exchange.rs::unauthenticated_setup_ends_in_ack_with_a_test_port_when_both_ends_opt_incrates/udpstp-core/tests/setup_mode1.rs::mode1_setup_between_our_cores_is_acceptedcrates/udpstp-core/tests/setup_rejections.rs::a_mode_2_request_is_accepted_in_mode_2
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:
crates/udpstp-core/tests/setup_responses.rs::each_rejection_code_ends_the_setup_with_its_own_failure
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:
crates/udpstp-core/tests/activation_exchange.rs::downstream_test_is_activated_on_the_test_portcrates/udpstp-core/tests/activation_exchange.rs::upstream_test_is_activated
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:
crates/udpstp-core/tests/downstream_load.rs::payload_content_is_random_when_asked_forcrates/udpstp-core/tests/sending_rate_table.rs::a_requested_starting_row_is_sent
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:
crates/udpstp/tests/loopback.rs::a_client_can_ask_for_algorithm_ccrates/udpstp-core/tests/activation_coercion.rs::an_unknown_rate_adjustment_algorithm_is_rejected_with_badparamcrates/udpstp-core/tests/activation_coercion.rs::algorithm_c_is_accepted
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:
crates/udpstp-core/tests/activation_coercion.rs::an_unknown_rate_adjustment_algorithm_is_rejected_with_badparamcrates/udpstp-core/tests/activation_mode1.rs::client_terminates_on_an_unknown_activation_response_code
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:
crates/udpstp-core/tests/upstream.rs::a_client_keeps_to_its_max_bandwidth_against_a_server_that_assigns_morecrates/udpstp-core/tests/upstream.rs::a_structure_within_the_max_bandwidth_is_sent_as_givencrates/udpstp-core/tests/upstream.rs::our_server_never_makes_the_client_cap_its_rate
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:
crates/udpstp-core/tests/setup_exchange.rs::unauthenticated_setup_ends_in_ack_with_a_test_port_when_both_ends_opt_incrates/udpstp-core/tests/setup_exchange.rs::server_without_opt_in_does_not_accept_an_unauthenticated_setupcrates/udpstp-core/tests/replay_cache.rs::unauthenticated_requests_bypass_the_cache
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:
crates/udpstp-core/tests/source_budget.rs::only_the_first_refusal_is_answeredcrates/udpstp-core/tests/multi_connection_checks.rs::a_duplicated_setup_request_is_answered_oncecrates/udpstp-core/src/replay.rs::a_full_cache_refuses_new_requests_until_entries_expire
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:
crates/udpstp-core/tests/upstream.rs::srstruct_values_beyond_the_sanity_limits_are_not_sentcrates/udpstp-core/tests/upstream.rs::the_sanity_limits_keep_their_edgescrates/udpstp-core/src/data_phase.rs::a_size_beyond_the_table_variant_is_refused
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rttcrates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pdus
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:
crates/udpstp-core/tests/algorithm_b.rs::in_fast_mode_each_clean_interval_adds_high_speed_delta_rows
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:
crates/udpstp/src/test_results.rs::report_layout_keeps_one_capacity_headline_and_one_aggregate_outcomecrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp/src/results.rs::an_upstream_testcrates/udpstp-core/tests/sub_interval_results.rs::an_upstream_client_reports_what_it_sent_per_st
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:
crates/udpstp-core/tests/quotas.rs::a_source_past_its_quota_of_tests_is_refused_with_connfailcrates/udpstp-core/tests/quotas.rs::a_source_past_its_quota_of_bytes_is_refusedcrates/udpstp-core/src/admission.rs::the_quota_refills_over_the_quota_periodcrates/udpstp-core/tests/quotas.rs::tests_set_up_together_past_the_quota_are_refused_at_activationcrates/udpstp-core/tests/quotas.rs::a_key_past_its_quota_is_refusedcrates/udpstp-core/src/admission.rs::operator_keys_are_exemptcrates/udpstp-core/src/admission.rs::a_test_lives_on_in_a_test_connection_waiting_for_its_socket
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:
crates/udpstp-core/tests/sending_rate_table.rs::row_zero_sends_fixed_size_datagrams_near_0_1_mbpscrates/udpstp-core/tests/sending_rate_table.rs::row_n_is_n_mbps_up_to_1_gbps
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:
crates/udpstp-core/tests/algorithm_b.rs::a_search_through_a_shallow_queue_alternates_around_the_bottleneckcrates/udpstp-core/tests/congestion_never_confirmed.rs::a_search_alternating_above_the_gigabit_row_met_the_limitcrates/udpstp-core/tests/congestion_never_confirmed.rs::a_search_alternating_near_12_gbit_s_met_the_limitcrates/udpstp-core/tests/maximum_through_drop_tail_queues.rs::above_the_gigabit_row_a_20_ms_queue_leaves_the_maximum_on_the_climb
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:
crates/udpstp-core/tests/algorithm_b.rs::rows_stay_within_the_table_and_fast_mode_stops_at_1_gbpscrates/udpstp-core/tests/algorithm_c.rs::the_jump_to_1_gbps_leaves_update_as_it_was
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:
crates/udpstp-core/tests/algorithm_b.rs::congestion_is_confirmed_by_back_to_back_errored_intervalscrates/udpstp-core/tests/algorithm_b.rs::intermittent_errored_intervals_do_not_confirm_congestioncrates/udpstp-core/tests/algorithm_b.rs::algorithm_b_never_leaves_single_step_mode
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:
crates/udpstp-core/tests/algorithm_b.rs::the_confirming_drop_stops_at_row_0_and_follows_the_row_it_starts_fromcrates/udpstp-core/tests/algorithm_b.rs::above_the_gigabit_row_every_step_is_one_row
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:
crates/udpstp-core/tests/algorithm_c.rs::fast_mode_doubles_the_row_on_every_second_clean_intervalcrates/udpstp-core/tests/algorithm_c.rs::doubling_the_row_doubles_the_rate
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:
crates/udpstp-core/tests/algorithm_c.rs::retries_come_after_5_then_10_then_15_single_steps_upcrates/udpstp-core/tests/algorithm_c.rs::a_retry_from_a_step_down_keeps_retry_thresh
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:
crates/udpstp-core/tests/algorithm_c.rs::clean_intervals_at_the_last_row_are_not_stepscrates/udpstp-core/tests/algorithm_c.rs::a_confirming_step_above_the_gigabit_row_can_retry
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:
crates/udpstp-core/tests/algorithm_c.rs::random_interval_sequences_follow_the_flowchartcrates/udpstp-core/tests/algorithm_c.rs::a_confirming_step_above_the_gigabit_row_can_retry
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:
crates/udpstp-core/tests/algorithm_c.rs::update_persists_across_a_retrycrates/udpstp-core/tests/algorithm_c.rs::the_jump_to_1_gbps_leaves_update_as_it_was
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:
crates/udpstp-core/tests/lost_status_backoff.rs::a_backoff_counts_as_an_errored_intervalcrates/udpstp-core/tests/lost_status_backoff.rs::lost_status_pdus_back_the_rate_off_and_confirm_congestioncrates/udpstp-core/tests/lost_status_backoff.rs::backoffs_stop_at_row_0_and_two_in_a_row_leave_fast_mode_on
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:
crates/udpstp-core/tests/lost_status_backoff.rs::a_long_path_gets_no_backoff_before_its_first_status_pducrates/udpstp-core/src/algorithm.rs::the_initial_w_covers_the_round_tripcrates/udpstp-core/src/algorithm.rs::the_initial_w_keeps_the_first_backoff_before_the_status_pdu_timeout
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:
crates/udpstp-core/tests/stale_status_pdus.rs::a_stale_status_pdu_does_not_steer_the_servercrates/udpstp-core/tests/stale_status_pdus.rs::a_stale_status_pdu_does_not_steer_the_clientcrates/udpstp-core/tests/stale_status_pdus.rs::a_stale_stop_does_not_end_the_testcrates/udpstp-core/tests/stale_status_pdus.rs::a_stale_status_pdu_still_counts_as_heard
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:
crates/udpstp-core/tests/stale_status_pdus.rs::spdu_seq_err_reports_the_status_pdus_missing_before_the_latestcrates/udpstp-core/tests/status_loss.rs::a_load_sender_counts_the_status_pdus_it_never_received
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:
crates/udpstp-core/tests/interval_classification.rs::a_stop_status_pdu_is_not_classifiedcrates/udpstp-core/tests/interval_classification.rs::an_upstream_server_classifies_the_status_pdus_it_sendscrates/udpstp-core/tests/stop_exchange.rs::load_pdus_arriving_after_stop1_still_count
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:
crates/udpstp-core/tests/upstream.rs::an_ipv6_client_sends_20_bytes_lesscrates/udpstp-core/tests/upstream.rs::an_ipv6_client_draws_random_sizes_from_32crates/udpstp-core/tests/sending_rate_table.rs::every_row_up_to_1_gbps_fits_the_rfc_sizes_and_ob_udpst_client_limits
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:
crates/udpstp-core/tests/mc_ident.rs::every_port_of_one_address_gives_its_own_non_zero_mc_identcrates/udpstp-core/tests/mc_ident.rs::hosts_behind_a_nat_draw_from_the_whole_rangecrates/udpstp-runtime/src/client.rs::clients_on_one_host_draw_different_mc_identscrates/udpstp/tests/loopback.rs::many_clients_on_one_host_starting_together_all_runcrates/udpstp-core/tests/replay_cache.rs::a_replayed_setup_request_is_dropped
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:
crates/udpstp-core/tests/setup_responses.rs::a_verified_setup_response_from_an_unapproved_address_is_ignoredcrates/udpstp-core/tests/setup_responses.rs::a_verified_setup_response_from_an_explicitly_authorized_address_is_acceptedcrates/udpstp-core/tests/setup_responses.rs::alternate_authorization_is_scoped_to_the_selected_servercrates/udpstp-core/tests/setup_responses.rs::an_authorized_alternate_still_needs_a_verified_matching_responsecrates/udpstp-core/tests/setup_responses.rs::an_alternate_keeps_the_locally_selected_ipv6_scopecrates/udpstp/tests/loopback.rs::only_locally_authorized_alternates_receive_activation_and_loadcrates/udpstp-core/tests/setup_responses.rs::an_unverified_setup_response_from_another_address_leaves_no_tracecrates/udpstp-core/tests/setup_responses.rs::an_unauthenticated_setup_response_from_another_address_is_ignored
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:
crates/udpstp-core/tests/setup_responses.rs::an_unverified_rejection_is_reported_but_the_timer_decidescrates/udpstp-core/tests/setup_responses.rs::an_unverified_rejection_for_another_test_connection_is_not_reportedcrates/udpstp-core/tests/auth_mode_default.rs::a_server_without_mode_2_is_reported_and_the_client_does_not_fall_back
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:
crates/udpstp-core/tests/setup_responses.rs::a_setup_response_with_the_wrong_version_pdu_id_or_cmd_request_is_ignoredcrates/udpstp-core/tests/setup_responses.rs::each_rejection_code_ends_the_setup_with_its_own_failure
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:
crates/udpstp-core/tests/multi_connection_start.rs::too_few_setups_abort_the_test_before_any_activationcrates/udpstp-core/tests/multi_connection_start.rs::a_lower_minimum_goes_on_with_the_setups_that_succeeded
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:
crates/udpstp-core/tests/multi_connection_start.rs::the_max_bandwidth_is_shared_between_the_test_connectionscrates/udpstp-core/tests/multi_connection_start.rs::a_max_bandwidth_below_1_mbps_a_test_connection_is_refusedcrates/udpstp/tests/cli.rs::multi_connection_options_are_validated
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:
crates/udpstp-core/tests/multi_connection_start.rs::an_unanswered_setup_is_given_up_once_enough_have_succeededcrates/udpstp-core/tests/multi_connection_start.rs::a_short_time_limit_still_activates_every_test_connection
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:
crates/udpstp-core/tests/multi_connection_failures.rs::the_test_goes_on_when_one_fails_and_enough_remaincrates/udpstp-core/tests/multi_connection_failures.rs::below_the_minimum_the_rest_are_stopped_and_releasedcrates/udpstp-core/tests/multi_connection_failures.rs::failures_are_counted_until_too_few_remain
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:
crates/udpstp-core/tests/multi_connection_failures.rs::test_connections_that_fall_silent_together_stop_the_testcrates/udpstp-core/tests/multi_connection_failures.rs::failures_apart_are_isolated_ones
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:
crates/udpstp-core/tests/sub_interval_results.rs::no_more_sub_intervals_are_reported_than_the_test_interval_holdscrates/udpstp-core/tests/sub_interval_results.rs::an_upstream_client_reports_each_sub_interval_from_sis_sav_oncecrates/udpstp-core/tests/sub_interval_results.rs::a_missing_final_sub_interval_is_missingcrates/udpstp-core/src/results.rs::a_missing_sub_interval_is_none
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:
crates/udpstp-core/src/results.rs::one_loss_always_meets_the_criterioncrates/udpstp-core/src/results.rs::each_active_test_connection_may_lose_one
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:
crates/udpstp-core/src/results.rs::a_sub_interval_with_nothing_received_meets_no_criterioncrates/udpstp/src/test_results.rs::a_test_whose_only_passing_sub_interval_is_empty_has_no_maximum
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:
crates/udpstp-core/tests/multi_connection_aggregation.rs::the_summed_capacity_never_exceeds_the_shared_bottleneckcrates/udpstp-core/tests/multi_connection_aggregation.rs::a_stalled_sub_interval_is_not_measuredcrates/udpstp-core/src/results.rs::test_connections_sum_by_sub_interval
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:
crates/udpstp/src/results.rs::a_completed_testcrates/udpstp/src/results.rs::terminal_layout_preserves_measurements_context_and_validity
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:
crates/udpstp-core/tests/setup_mode1.rs::clocks_five_seconds_apart_are_within_the_windowcrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_more_than_five_seconds_offcrates/udpstp-core/tests/setup_mode1.rs::setup_is_accepted_across_the_2106_wraparoundcrates/udpstp-core/tests/activation_mode1.rs::server_accepts_an_activation_time_5_s_off
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::a_wrong_key_is_answered_with_authfailcrates/udpstp-core/tests/troubleshooting_mode.rs::an_unknown_key_id_is_answered_with_authfailcrates/udpstp-core/tests/troubleshooting_mode.rs::a_time_outside_the_window_is_answered_with_a_signed_authtimecrates/udpstp-core/tests/troubleshooting_mode.rs::an_unauthenticated_request_is_answered_with_authreqcrates/udpstp-core/tests/troubleshooting_mode.rs::an_authenticated_request_to_a_server_without_keys_is_answered_with_authnccrates/udpstp-core/tests/troubleshooting_mode.rs::an_unsupported_mode_is_answered_with_authinv
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::at_most_10_failures_a_second_are_answeredcrates/udpstp-core/tests/troubleshooting_mode.rs::while_on_the_server_warns_every_minutecrates/udpstp-core/tests/troubleshooting_mode.rs::a_replay_stays_silentcrates/udpstp-core/tests/troubleshooting_mode.rs::a_failed_activation_request_stays_silent_in_troubleshooting_mode
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:
crates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_per_test_cap_is_refused_with_connfailcrates/udpstp-core/tests/admission_limits.rs::the_per_test_cap_is_24_by_defaultcrates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_global_cap_is_refused_with_connfailcrates/udpstp-core/tests/admission_limits.rs::a_setup_whose_socket_cant_be_opened_is_refused_and_releasedcrates/udpstp-core/tests/source_budget.rs::a_request_over_its_sources_budget_gets_a_signed_connfail
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:
crates/udpstp-core/src/admission.rs::a_test_kept_alive_cannot_start_new_test_connections_past_its_capcrates/udpstp-core/src/admission.rs::a_test_waiting_on_one_test_connection_keeps_its_closed_count
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:
crates/udpstp-core/tests/multi_connection_checks.rs::a_zero_mc_ident_is_refusedcrates/udpstp-core/src/admission.rs::an_mc_index_not_below_mc_count_is_refusedcrates/udpstp-core/tests/multi_connection_checks.rs::an_mc_index_already_in_use_in_the_test_is_refusedcrates/udpstp-core/src/admission.rs::a_test_connection_with_another_mc_count_is_refusedcrates/udpstp-core/src/admission.rs::a_test_connection_with_another_auth_mode_is_refusedcrates/udpstp-core/src/admission.rs::a_test_connection_with_another_key_id_is_refusedcrates/udpstp-core/src/admission.rs::in_unauthenticated_mode_the_key_id_is_not_comparedcrates/udpstp-core/src/admission.rs::a_test_runs_without_every_mc_index
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:
crates/udpstp-core/tests/replay_cache.rs::a_replayed_setup_request_is_droppedcrates/udpstp-core/tests/replay_cache.rs::a_setup_request_replayed_after_its_test_connection_ended_is_droppedcrates/udpstp-core/tests/replay_cache.rs::a_request_refused_by_its_sources_budget_isnt_rememberedcrates/udpstp-core/tests/setup_rejections.rs::a_replayed_rejected_request_gets_one_response
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:
crates/udpstp-core/tests/replay_cache.rs::a_replayed_activation_request_is_dropped_at_a_sibling_test_connectioncrates/udpstp-core/tests/replay_cache.rs::an_activation_request_from_another_ip_address_is_dropped
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:
crates/udpstp-core/src/replay.rs::an_entry_lasts_11_scrates/udpstp-core/src/replay.rs::a_full_cache_refuses_new_requests_until_entries_expirecrates/udpstp-core/src/replay.rs::the_capacity_covers_what_the_budgets_let_through
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:
crates/udpstp-core/tests/unverified_budget.rs::setup_requests_beyond_the_budget_are_dropped_silentlycrates/udpstp-core/tests/unverified_budget.rs::the_budget_is_20000_a_second_by_defaultcrates/udpstp-core/tests/unverified_budget.rs::setup_requests_that_arrive_together_pass_or_fail_togethercrates/udpstp-core/tests/unverified_budget.rs::a_request_with_an_unknown_key_id_doesnt_spend_the_budgetcrates/udpstp-core/tests/unverified_budget.rs::an_unauthenticated_request_spends_the_budget
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:
crates/udpstp-core/tests/source_budget.rs::requests_that_fail_the_hmac_dont_spend_the_budgetcrates/udpstp-core/tests/source_budget.rs::a_whole_multi_connection_test_fits_the_default_burstcrates/udpstp-core/tests/source_budget.rs::unauthenticated_requests_dont_spend_the_budgetcrates/udpstp-core/tests/source_budget.rs::only_the_first_refusal_is_answered
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:
crates/udpstp-core/tests/key_budget.rs::a_keys_setup_requests_over_its_budget_are_dropped_silentlycrates/udpstp-core/tests/key_budget.rs::an_operator_keys_setup_requests_spend_its_budget_toocrates/udpstp-core/tests/key_budget.rs::a_flood_on_one_key_leaves_another_keys_budget_alonecrates/udpstp-core/tests/key_budget.rs::unauthenticated_setup_requests_share_one_budgetcrates/udpstp-core/tests/key_budget.rs::a_replayed_setup_request_spends_no_keys_budget
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:
crates/udpstp-core/tests/tests_per_source.rs::a_second_test_from_the_same_source_is_refused_at_setupcrates/udpstp-core/tests/tests_per_source.rs::of_two_tests_set_up_together_the_second_is_refused_at_activationcrates/udpstp-core/src/admission.rs::the_test_connections_of_one_test_are_one_testcrates/udpstp-core/src/admission.rs::only_activated_tests_count_against_their_sourcecrates/udpstp-core/src/admission.rs::a_source_is_an_ipv4_address_or_an_ipv6_64
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:
crates/udpstp-core/src/admission.rs::the_bandwidth_cap_holds_per_direction_with_usdir_maskedcrates/udpstp-core/tests/admission_limits.rs::a_test_connection_over_the_bandwidth_cap_is_refused_with_capexccrates/udpstp-core/tests/admission_limits.rs::a_server_that_requires_max_bandwidth_refuses_its_absence_with_nomaxbw
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:
crates/udpstp-core/tests/admission_limits.rs::an_activation_in_the_other_direction_than_the_setup_stated_is_refusedcrates/udpstp-core/src/admission.rs::an_upstream_test_stating_no_max_bandwidth_may_leave_usdir_clearcrates/udpstp-core/src/admission.rs::without_a_bandwidth_cap_the_direction_bit_is_not_checked
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:
crates/udpstp-core/src/admission.rs::an_unactivated_setup_holds_no_ratecrates/udpstp-core/tests/admission_limits.rs::of_two_setups_racing_past_the_cap_the_first_activated_keeps_itcrates/udpstp-core/src/admission.rs::a_multi_connection_test_is_admitted_or_refused_wholecrates/udpstp-core/src/admission.rs::an_upstream_test_that_no_longer_fits_is_refused_at_activation_and_releasedcrates/udpstp-core/src/admission.rs::an_activated_tests_test_connections_hold_their_rate_before_their_sockets_opencrates/udpstp-core/src/admission.rs::a_first_activation_reserves_each_direction_its_test_states
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:
crates/udpstp-core/tests/test_share.rs::a_consumer_s_test_may_hold_half_the_capcrates/udpstp-core/src/admission.rs::the_share_counts_every_test_connection_of_the_testcrates/udpstp-core/src/admission.rs::an_operator_key_may_hold_the_whole_cap
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:
crates/udpstp-core/tests/unactivated_share.rs::a_new_setup_evicts_the_oldest_unactivated_test_connectioncrates/udpstp-core/src/admission.rs::activated_test_connections_dont_countcrates/udpstp-core/src/admission.rs::a_whole_multi_connection_test_fits_the_sharecrates/udpstp-core/src/admission.rs::a_setup_refused_anyway_evicts_no_onecrates/udpstp-core/src/admission.rs::a_setup_the_rate_cap_refuses_evicts_no_one_from_a_full_capcrates/udpstp-core/src/admission.rs::a_flooding_key_evicts_its_own_unactivated_test_connectionscrates/udpstp-core/src/admission.rs::a_busy_server_still_evicts_a_flooding_keys_unactivated_test_connectionscrates/udpstp-core/src/admission.rs::a_test_that_no_longer_fits_cannot_activate_a_remaining_siblingcrates/udpstp-core/src/admission.rs::the_first_activation_reserves_siblings_waiting_for_their_socketscrates/udpstp-core/src/admission.rs::the_unactivated_share_can_exceed_the_global_capcrates/udpstp-core/tests/admission_limits.rs::of_two_setups_racing_for_one_place_only_one_activatescrates/udpstp/tests/cli.rs::a_ephemeral_port_range_smaller_than_the_cap_plus_share_is_a_configuration_errorcrates/udpstp/tests/cli.rs::the_port_range_accounts_for_configured_and_default_shares_without_overflowcrates/udpstp-core/src/admission.rs::unauthenticated_test_connections_are_one_groupcrates/udpstp-core/src/admission.rs::a_multi_connection_test_survives_another_keys_floodcrates/udpstp-core/src/admission.rs::of_two_groups_holding_as_many_the_older_gives_up_its_oldestcrates/udpstp-core/src/admission.rs::a_group_holding_the_most_only_in_waiting_setups_evicts_no_one_elsecrates/udpstp-core/tests/unactivated_share.rs::another_keys_flood_cannot_evict_a_clients_unactivated_test_connectioncrates/udpstp-core/tests/unactivated_share.rs::an_unauthenticated_flood_cannot_evict_a_keyed_clients_unactivated_test_connectioncrates/udpstp-core/tests/unactivated_share.rs::a_flood_on_the_clients_own_key_still_evicts_it
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:
crates/udpstp-core/tests/connection_locking.rs::the_first_verified_activation_request_connects_the_socketcrates/udpstp-core/tests/connection_locking.rs::a_request_from_another_ip_address_connects_nothingcrates/udpstp-core/tests/connection_locking.rs::a_request_failing_verification_connects_nothingcrates/udpstp-core/tests/connection_locking.rs::a_nat_that_changes_the_port_is_followed
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:
crates/udpstp-core/tests/activation_exchange.rs::a_test_connection_asking_for_the_other_direction_is_rejectedcrates/udpstp-core/tests/activation_exchange.rs::test_connections_of_one_test_with_the_same_direction_are_activated
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:
crates/udpstp-core/tests/fixed_rate.rs::a_fixed_rate_test_is_refused_without_an_operator_keycrates/udpstp-core/tests/fixed_rate.rs::an_operator_runs_a_fixed_rate_test_at_the_requested_rowcrates/udpstp-core/tests/fixed_rate.rs::a_refused_downstream_fixed_rate_test_sends_no_load
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:
crates/udpstp-core/tests/fixed_rate.rs::a_row_beyond_the_table_is_coerced_to_the_last_rowcrates/udpstp-core/tests/activation_coercion.rs::a_coerced_row_is_reported_at_both_ends
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:
crates/udpstp-core/tests/algorithm_capped.rs::fast_mode_stops_at_the_row_matching_max_bandwidthcrates/udpstp-core/tests/algorithm_capped.rs::single_steps_stop_at_the_capcrates/udpstp-core/tests/algorithm_capped.rs::the_cap_masks_the_direction_bitcrates/udpstp-core/tests/algorithm_capped.rs::a_max_bandwidth_of_0_states_no_capcrates/udpstp-core/tests/algorithm_capped.rs::a_cap_above_1_gbps_stops_at_the_row_within_it
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:
crates/udpstp-core/tests/algorithm_capped.rs::a_start_row_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_fixed_rate_above_the_cap_is_coerced_to_itcrates/udpstp-core/tests/algorithm_capped.rs::a_start_row_at_the_cap_is_not_coerced
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:
crates/udpstp-core/tests/start_row_cap.rs::a_consumer_s_start_row_is_coerced_to_the_capcrates/udpstp-core/tests/start_row_cap.rs::an_operator_key_is_exemptcrates/udpstp-core/tests/start_row_cap.rs::by_default_a_search_starts_at_row_100_at_mostcrates/udpstp-core/tests/start_row_cap.rs::a_capped_search_still_climbs_past_the_cap
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:
crates/udpstp-core/tests/test_interval_limits.rs::an_ordinary_key_gets_at_most_60_scrates/udpstp-core/tests/test_interval_limits.rs::an_operator_key_allows_its_own_maximum_with_rtt_variationcrates/udpstp-core/tests/test_interval_limits.rs::the_client_runs_a_long_test_its_operator_key_allowscrates/udpstp-core/tests/test_interval_limits.rs::the_client_refuses_a_longer_test_than_it_asked_for
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:
crates/udpstp-core/tests/test_interval_limits.rs::slow_adj_thresh_times_trial_int_is_at_most_500_mscrates/udpstp-core/tests/test_interval_limits.rs::an_operator_key_allows_its_own_maximum_with_rtt_variation
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:
crates/udpstp-core/tests/activation_coercion.rs::the_server_clears_a_requested_ecn_codepointcrates/udpstp-core/tests/dscp.rs::a_server_allowing_only_the_default_coerces_a_priority_dscp_to_0crates/udpstp-core/tests/dscp.rs::an_operator_key_may_ask_for_any_dscpcrates/udpstp-core/tests/dscp.rs::an_operator_key_may_not_ask_for_network_control_by_default
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:
crates/udpstp-core/tests/activation_coercion.rs::the_client_refuses_a_dscp_it_did_not_ask_forcrates/udpstp-core/tests/activation_coercion.rs::the_client_sends_not_ect_whatever_the_server_answers
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:
crates/udpstp-core/tests/table_variants.rs::a_client_asking_for_the_server_s_modifiers_is_servedcrates/udpstp-core/tests/table_variants.rs::srstruct_packet_sizes_follow_the_modifierscrates/udpstp-core/tests/setup_rejections.rs::a_jumbo_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/setup_rejections.rs::a_traditional_mtu_setting_the_server_doesnt_use_is_rejectedcrates/udpstp-core/tests/setup_rejections.rs::traditional_mtu_without_jumbo_is_rejected_as_a_jumbo_mismatchcrates/udpstp-core/tests/sending_rate_table.rs::row_n_is_n_mbps_up_to_1_gbps
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::an_upstream_client_stops_when_status_pdus_stopcrates/udpstp-core/tests/timeouts.rs::the_watchdog_ends_the_test_connection_3_s_after_the_last_pdu
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:
crates/udpstp-core/tests/timeouts.rs::a_downstream_path_that_breaks_stops_within_1_s_and_ends_at_3_scrates/udpstp-core/tests/timeouts.rs::rx_stopped_clears_when_traffic_comes_back_in_time
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:
crates/udpstp-core/tests/stop_exchange.rs::only_stop2_stops_the_client_and_later_load_pdus_are_ignoredcrates/udpstp-core/tests/stop_exchange.rs::the_server_ends_on_a_stop1_from_the_client
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:
crates/udpstp-core/tests/stop_exchange.rs::the_test_stops_gracefully_when_the_servers_test_interval_endscrates/udpstp-core/tests/stop_exchange.rs::only_stop2_stops_the_client_and_later_load_pdus_are_ignoredcrates/udpstp-core/tests/server_shutdown.rs::shutdown_completes_when_stop_is_the_first_downstream_load_pdu
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:
crates/udpstp-core/tests/stop_exchange.rs::a_client_that_refuses_a_coercion_stops_the_server_at_oncecrates/udpstp-core/tests/stop_exchange.rs::a_client_stopping_its_data_phase_tells_the_server_at_oncecrates/udpstp-core/tests/upstream.rs::an_upstream_client_that_stops_early_sends_a_stop2_load_pdu
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:
crates/udpstp-core/tests/server_shutdown.rs::a_stopping_server_stops_each_data_phase_and_then_releases_itcrates/udpstp-core/tests/server_shutdown.rs::shutdown_completes_when_stop_is_the_first_downstream_load_pducrates/udpstp-core/tests/server_shutdown.rs::shutdown_sends_stop_before_the_first_upstream_load_pducrates/udpstp-core/tests/server_shutdown.rs::shutdown_does_not_send_stop_after_the_receive_timeoutcrates/udpstp/tests/loopback.rs::a_server_stopped_by_a_signal_ends_its_tests_and_reports_them(runs only with cfg(unix))
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:
crates/udpstp-core/tests/upstream.rs::a_random_size_add_on_is_sent_at_random_sizescrates/udpstp-core/tests/upstream.rs::an_ipv6_client_draws_random_sizes_from_32
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:
crates/udpstp-core/tests/downstream_load.rs::lpdu_time_is_the_send_time_and_ignores_wall_clock_steps
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:
crates/udpstp-core/tests/partial_sends.rs::deferred_datagrams_keep_their_sequence_numbers_and_count_no_losscrates/udpstp-core/tests/partial_sends.rs::deferred_datagrams_carry_the_time_they_are_sentcrates/udpstp-core/tests/partial_sends.rs::a_whole_deferred_burst_withholds_the_nextcrates/udpstp-core/tests/partial_sends.rs::a_host_that_takes_nothing_withholds_every_burstcrates/udpstp-core/tests/partial_sends.rs::a_stop_sends_the_deferred_datagrams_first
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:
crates/udpstp-core/tests/status_pdus.rs::sub_intervals_are_counted_from_the_first_status_pducrates/udpstp-core/tests/status_pdus.rs::a_late_status_pdu_does_not_shift_the_next_sub_interval
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:
crates/udpstp-core/tests/status_pdus.rs::status_pdus_follow_the_first_load_pdu_every_trial_intervalcrates/udpstp-core/tests/status_pdus.rs::each_status_pdu_counts_its_trial_interval
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:
crates/udpstp-core/tests/status_pdus.rs::bytes_are_counted_from_the_datagram_not_its_udp_payload_fieldcrates/udpstp-core/tests/sub_interval_results.rs::a_forged_udp_payload_field_doesnt_inflate_the_capacity
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:
crates/udpstp-core/tests/status_pdus.rs::the_load_sender_tolerates_auth_fields_in_mode1_status_pduscrates/udpstp-core/tests/status_pdus.rs::status_pdus_carry_the_auth_mode_and_zero_auth_fields
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:
crates/udpstp-core/tests/sequence_errors.rs::loss_counts_every_missing_load_pducrates/udpstp-core/tests/sequence_errors.rs::the_rfc_example_counts_four_out_of_ordercrates/udpstp-core/tests/sequence_errors.rs::a_first_load_pdu_numbered_0_counts_no_losscrates/udpstp-core/tests/sequence_errors.rs::a_far_first_load_pdu_counts_no_loss
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:
crates/udpstp-core/tests/sequence_errors.rs::a_late_load_pdu_in_the_next_trial_interval_leaves_the_loss_countedcrates/udpstp-core/tests/sequence_errors.rs::a_late_load_pdu_does_not_cancel_a_later_loss
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:
crates/udpstp-core/tests/sequence_errors.rs::half_the_sequence_space_ahead_counts_as_latecrates/udpstp-core/tests/sequence_errors.rs::sequence_errors_are_counted_across_the_wrap
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:
crates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pducrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rttcrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_is_the_time_since_the_status_pdu_arrived
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:
crates/udpstp-core/tests/delay_variation.rs::delay_variation_is_rounded_per_load_pducrates/udpstp-core/tests/delay_variation.rs::clock_delta_min_saturates
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:
crates/udpstp-core/tests/delay_variation.rs::kernel_receive_timestamps_give_the_receive_timecrates/udpstp-core/src/data_phase.rs::status_pdu_receive_times_are_clamped
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:
crates/udpstp-core/tests/delay_variation.rs::rtt_is_measured_from_echoed_status_pduscrates/udpstp-core/tests/delay_variation.rs::rtt_resp_delay_adjusts_the_rtt
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:
crates/udpstp-core/tests/setup_addresses.rs::a_setup_request_from_a_group_address_is_dropped_silentlycrates/udpstp-core/tests/setup_addresses.rs::a_setup_request_to_a_group_address_is_dropped_silentlycrates/udpstp-core/tests/setup_addresses.rs::unicast_sources_are_answeredcrates/udpstp-core/tests/setup_addresses.rs::an_activation_request_from_a_group_address_is_dropped
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:
crates/udpstp-wire/tests/kdf_kat.rs::derived_keys_match_every_openssl_vectorcrates/udpstp-wire/tests/kdf_kat.rs::vectors_cover_authunixtime_extremes_and_zero_bytes_in_keys
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:
crates/udpstp-core/tests/key_table.rs::a_key_outside_its_accept_lifetime_is_an_authentication_failurecrates/udpstp-core/tests/key_table.rs::a_key_is_accepted_on_the_edges_of_its_accept_lifetimecrates/udpstp-core/tests/key_table.rs::a_shared_key_id_uses_the_row_valid_at_the_timecrates/udpstp-core/tests/key_table.rs::rows_sharing_a_key_id_must_not_overlapcrates/udpstp-core/tests/key_table.rs::the_client_sends_with_the_newest_valid_keycrates/udpstp-core/tests/key_table.rs::a_client_without_a_valid_send_key_does_not_startcrates/udpstp/tests/loopback.rs::the_server_ignores_a_key_past_its_accept_lifetime
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:
crates/udpstp/tests/key_files.rs::the_server_loads_a_toml_key_tablecrates/udpstp/tests/key_files.rs::an_invalid_key_file_exits_with_51_and_says_where
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:
crates/udpstp/tests/key_files.rs::the_server_loads_a_toml_key_tablecrates/udpstp/tests/key_files.rs::an_invalid_key_file_exits_with_51_and_says_wherecrates/udpstp/tests/loopback.rs::fixed_rate_tests_complete_over_loopback
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:
crates/udpstp/tests/key_files.rs::the_server_loads_a_toml_key_tablecrates/udpstp/tests/loopback.rs::a_mode1_test_runs_with_a_toml_key_table_and_an_obudpst_key_file
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:
crates/udpstp-core/tests/key_table.rs::equally_new_send_keys_need_a_pinned_key_idcrates/udpstp/tests/key_files.rs::an_ambiguous_key_file_asks_for_a_key_id
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:
crates/udpstp-core/src/budget.rs::requests_sharing_a_row_and_time_cost_one_kdf_runcrates/udpstp-core/tests/setup_mode1.rs::server_silently_drops_a_setup_signed_from_a_different_shared_key
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:
crates/udpstp-core/tests/interval_classification.rs::the_maximum_is_the_default_and_the_average_a_settingcrates/udpstp-core/tests/interval_classification.rs::the_average_rounds_and_clean_is_strictly_below_the_low_thresholdcrates/udpstp-core/tests/interval_classification.rs::without_one_way_delay_the_latest_rtt_sample_is_delay_var
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:
crates/udpstp-core/tests/auth_mode_default.rs::a_server_without_mode_2_is_reported_and_the_client_does_not_fall_backcrates/udpstp-core/tests/auth_mode_default.rs::mode_1_is_used_only_with_the_server_it_was_chosen_for
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:
crates/udpstp/src/test_connection_end.rs::every_exit_statuscrates/udpstp/src/exit.rs::a_tests_status_from_its_test_connections
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:
crates/udpstp-core/tests/multi_connection_failures.rs::an_interrupted_test_stops_every_test_connection_with_the_stop_exchangecrates/udpstp-core/tests/multi_connection_failures.rs::an_interrupt_after_the_test_ended_is_no_abortcrates/udpstp/src/exit.rs::a_tests_status_from_its_test_connectionscrates/udpstp/tests/loopback.rs::an_interrupted_client_stops_the_test_and_reports_it(runs only with cfg(unix))crates/udpstp/tests/loopback.rs::a_client_whose_stdout_closes_stops_cleanlycrates/udpstp/tests/loopback.rs::a_client_whose_stdout_closes_after_activation_stops_cleanlycrates/udpstp/tests/loopback.rs::a_client_started_with_sigint_ignored_keeps_running(runs only with cfg(unix))
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:
crates/udpstp/src/test_connection_end.rs::every_exit_statuscrates/udpstp/src/exit.rs::a_tests_status_from_its_test_connections
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:
crates/udpstp/src/test_results.rs::an_interrupted_tests_json_keeps_its_measured_sub_intervalscrates/udpstp/src/test_results.rs::an_incomplete_tests_json_keeps_missing_measurements_explicitcrates/udpstp/tests/loopback.rs::an_interrupted_client_stops_the_test_and_reports_it(runs only with cfg(unix))
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:
crates/udpstp-core/tests/troubleshooting_mode.rs::by_default_authentication_failures_are_silentcrates/udpstp-core/tests/setup_rejections.rs::a_request_whose_digest_fails_gets_no_response
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:
crates/udpstp-core/tests/setup_rejections.rs::a_bad_protocol_version_is_rejected_with_the_expected_versioncrates/udpstp-core/tests/setup_rejections.rs::an_unauthenticated_request_gets_unsigned_rejections
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:
crates/udpstp-core/tests/setup_rejections.rs::a_disallowed_unauthenticated_request_is_silent_whatever_its_optionscrates/udpstp-core/tests/setup_rejections.rs::the_options_are_judged_before_the_multi_connection_fieldscrates/udpstp-core/src/admission.rs::a_setup_is_refused_by_the_first_check_it_fails
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:
crates/udpstp-core/tests/upstream.rs::the_server_stops_a_client_sending_over_its_ratecrates/udpstp-core/tests/algorithm_b.rs::an_upstream_search_through_a_deep_buffer_is_not_stopped_over_rate
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:
crates/udpstp-core/tests/downstream_load.rs::a_stalled_engine_skips_missed_bursts_and_reports_themcrates/udpstp-core/tests/downstream_load.rs::bursts_missed_by_less_than_the_catch_up_budget_are_all_sentcrates/udpstp-core/tests/downstream_load.rs::bursts_missed_by_more_than_the_catch_up_budget_are_skippedcrates/udpstp-core/tests/downstream_load.rs::a_burst_exactly_the_catch_up_budget_late_is_skipped
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:
crates/udpstp-core/tests/sequence_errors.rs::a_jump_over_1024_waits_for_the_next_load_pducrates/udpstp-core/tests/sequence_errors.rs::a_forged_first_load_pdu_is_ignoredcrates/udpstp-core/tests/sequence_errors.rs::counting_follows_a_run_of_32_out_of_order_load_pduscrates/udpstp-core/tests/sequence_errors.rs::a_resync_lets_the_interval_take_back_its_own_loss
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:
crates/udpstp-core/tests/control_checksum.rs::only_a_configured_receiver_verifies_and_only_a_populated_fieldcrates/udpstp-core/tests/downstream_load.rs::the_client_verifies_a_populated_checksum_when_configured
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:
crates/udpstp/tests/key_files.rs::the_server_loads_an_obudpst_key_filecrates/udpstp/tests/key_files.rs::an_invalid_key_file_exits_with_51_and_says_wherecrates/udpstp/tests/loopback.rs::a_mode1_test_runs_with_a_toml_key_table_and_an_obudpst_key_filecrates/udpstp/tests/loopback.rs::an_obudpst_key_need_not_be_utf8
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:
crates/udpstp/tests/key_files.rs::a_key_file_others_can_read_is_refused(runs only with cfg(unix))crates/udpstp/tests/key_files.rs::a_key_file_must_be_a_regular_file(runs only with cfg(unix))crates/udpstp/tests/key_files.rs::a_private_key_file_loads_on_windows(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::native_acl_matrix_uses_the_production_assessment(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::both_roles_refuse_mismatch_and_unverified_without_key_material(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::native_inspection_failure_and_read_sharing(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::a_private_file_supports_a_successful_authenticated_test(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::manual_setup_removes_pre_existing_outsider_grants(runs only with cfg(windows))crates/udpstp/tests/windows_key_permissions.rs::refused_server_keys_reach_stderr_and_the_daily_log(runs only with cfg(windows))crates/udpstp/tests/cli.rs::settings_only_prints_settings_without_loading_keys_or_opening_logscrates/udpstp/tests/cli.rs::settings_only_requires_printing_and_cannot_run_as_a_service
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:
crates/udpstp/tests/key_files.rs::a_key_file_owned_by_another_user_is_refused(runs only with cfg(unix))
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
Lprefix, for exampleRFC9946-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 leadingNote: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
ticketgoes. - 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 = trueand 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
interpretationrecord withobudpst; 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 itsreference(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 acfg, 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_payload1for a nested one), and the conditions under which it shows, fromCONDITIONSinxtask/src/records.rs.interop-masks.tomllists 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
Lseries 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 aticket; one whose only tests don’t run on the gating platform and isn’tnightly;nightlyon 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--flagthe 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 withoutobudpst, or naming an unknown field or condition; aconformance: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.
- Key files. Both ends need the same Shared Key. A file whose name ends in
.tomlis read as udpstp’s Key Table; any other file is read as an OB-UDPSTudpst.keysfile (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-configprints the result (crates/udpstp/src/config.rs). Hosting a Server lists every setting and its default. - Client options.
--direction,--test-interval,--algorithm,--fixed-rateor--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-ratioand--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 (itsTestPlanand 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’sServerOptions, with the warnings to show first and the configuration errors that stop it, each with its exit status, as data;main.rsprints 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
| Crate | Role |
|---|---|
udpstp-wire | PDU layouts and codecs, checksum, HMAC-SHA-256 and the KDF. No protocol decisions: decoding checks only size and pduId. |
udpstp-core | The 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-runtime | Sockets, clocks, the Tokio Control Phase loops and the Data Phase Engine. Makes no protocol decisions. |
udpstp | The binary: CLI, Server configuration, key files, exit codes, output. |
udpstp-key-permissions | Windows application support: borrowed-file native inspection, owned descriptor decoding and the supported private-file policy (ADR 0015). No protocol decisions. |
udpstp-req | The #[req("ID")] proc-macro attribute that ties tests to Requirements. |
xtask | cargo xtask req (Requirement traceability) and known-answer test tooling. |
bench-rounds | The 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-bench | udpstp’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.
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.
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):
| Direction | Type | Meaning |
|---|---|---|
| in | handle_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. |
| in | handle_socket_opened / handle_socket_closed | A 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. |
| in | handle_timeout(Now) | The deadline from poll_timeout has passed. |
| in | handle_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. |
| out | Output::Transmit(Transmit) | Send a payload from a SocketId to an address. |
| out | Output::OpenSocket / Output::CloseSocket | Bind a Test Connection’s socket (the Ephemeral Port), or close one. |
| out | Output::ConnectSocket | Connect 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. |
| out | Output::StartDataPhase | Hand the Test Connection to a Data Phase Engine and accept datagrams only from peer. |
| out | Output::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 dueSendPlan: a heap-free,CopySend Plan of at mostMAX_RUNSruns of equal-sized datagrams with contiguous sequence numbers.horizonmust 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)takesIcmp,MsgSizeorTimeExceededand counts them asDataEvent::IcmpErrors,PathMtuExceededandHopLimitExceeded(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 asEvent::PathError.stop(Now)ends the Data Phase.poll_event()returns coalescedDataEvents, ending withDataEvent::Ended(EndReason), which a Client’sDataEvent::TrialIntervalsCountedcomes 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.
Step by step, with the code that handles it:
-
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--unauthenticatedit fails withControlPhaseFailure::NoAuthenticationConfigured. -
Verification and Admission.
Server::handle_setup_request(crates/udpstp-core/src/server.rs) checks a Setup Request in the order the Conformance Recordsetup-check-orderrecords. 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::SetupRequestIgnoredwithSetupIgnoreReason::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’schecksum::control_validsaying whether the field matches; senders fill it last withchecksum::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 asEvent::SetupRequestSilentlyRejectedwith aSilentRejectionReason; withtroubleshootingon, 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 atsource_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_burstandkey_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_sourceallows (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_bandwidthis 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_openedsends 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, untilhandle_socket_closed; a socket that can’t be opened is refused with CONNFAIL.Admissionkeeps 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. - A duplicate of an admitted Setup Request from the same address is ignored, not refused (
-
Null Request. Right after the Setup Response,
handle_socket_openedsends 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, orNullRequestIgnored) and never answers; nothing depends on it. It leaves from the Ephemeral Port, which is bound to the address the Setup Request arrived on. -
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_responsethen 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 withControlPhaseFailure::SetupRejected(SetupRejectionCode), and an undefined code withUnknownSetupResponse. The runtime sends only destinations selected by the core. -
Activation Response.
Server::handle_activation_requestchecks 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 theDataPhase. -
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.
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_rateafter 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’sdelay_var_statisticsetting 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::TrialIntervalsCountedsays 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.
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):
- If the stop flag is set, call
DataPhase::stoponce. - Send every due Send Plan whole:
poll_burst,write_datagramsjust before the send, the datagrams (Socket::send_plan), thenreport_sentwith 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). - Read what has arrived (
Socket::receive), so a Status PDU waiting in the socket counts before any timeout is judged. - Handle a due timeout, then send every due Status PDU (
poll_transmit,Socket::send). - Forward
DataEvents to the Control Phase. AfterDataEvent::Ended, exit. - Wait until the next deadline (
next_burst_atorpoll_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 refusesppoll(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):sendmmsgcalls 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 refusedsendmmsg(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 itssendmmsgcalls 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):recvmmsgcalls of up to 64 datagrams, a Load Receiver’s header-only withMSG_TRUNC, each with itsSO_TIMESTAMPNSreceive 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’srecvmmsgneeds (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 refusedrecvmmsg(ENOSYS, EPERM, EINVAL, EOPNOTSUPP) turns batched reading off, for onerecv_fromper 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, asendmmsgwith or without GSO, a send whose messages carry the numbers of their kernel send timestamps, a read, arecvmmsg, appoll, 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 andppoll, 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_TIMESTAMPINGwithOPT_IDandOPT_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 anSCM_TS_OPT_IDcontrol 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’ssendmmsgshares one set of control messages across a call, so the Linux adapter then sends each message of a batch with asendmsgcall of its own (socket2’s) and reports how many the kernel took, assendmmsgwould; it offers batches that don’t go in onesendmmsgcall, so the socket reports UDP GSO withoutsendmmsgamong 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 insendmmsgcalls 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-countleaves the numbering to the kernel on every kernel (the socket’sSettings::explicit_numbersoff), so a result can be checked on that send path, with batches insendmmsgcalls. 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.
Portablesends and reads one datagram per call with std’s socket (Windows, macOS).Linuxadds the batched calls,ppoll, the error queue, send timestamps and Receive Buffer Overflows (socket/overflows.rs), and makes its single sends and reads withPortable’s calls. It counts the overflows from the process’s own UDP tables in/proc/self/netwhen the Data Phase ends, or, where it can’t read them (since Linux 7.2, a/procmounted withsubset=pidby a mounter withoutCAP_NET_ADMINover 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. AnOpenrecipe given to the engine at spawn (engine::spawn_with) opens the adapter on the Engine Thread, since nix’s header sets aren’tSend: 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’sUDPSTP_ENGINE_THREADSandUDPSTP_ENGINE_GROUPING_US) are read by the binary once, instart::engine_settings(crates/udpstp/src/start/mod.rs), and passed as one value,EngineSettings, inServerOptionsand torun_test, which give it to each engine at spawn. The runtime reads none of them; only the test-onlyUDPSTP_FAULTS, which belongs to itsfault-injectionfeature.
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).
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.
- Ends of the Data Phase.
DataEvent::Endedcarries anEndReason:Completed,Unconfirmed,TestIntervalElapsed,StoppedByClient,Stopped,WatchdogorOverRate.EndReason::completessays which of them complete the Test Connection:Completed, andTestIntervalElapsed, 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_closedremoves the Test Connection, releases its Admission and emitsEvent::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 withoutDataEvent::Endedas a failed Test Connection. On the Client,run_testreturns once every Test Connection’s engine has exited and the Multi-Connection Coordinator has no deadline left. The Client ends in itsState::Endedon a rejection or a Control Phase failure. When the Server administrator stops the Server,Server::handle_shutdownstops 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
crates/udpstp-core/src/lib.rs: theEndpointtrait and the public API.crates/udpstp-core/src/io.rs,time.rs,event.rs: what crosses the boundary.crates/udpstp-core/src/server.rsandclient.rs: the Setup and Activation exchanges.crates/udpstp-core/src/data_phase.rs:DataPhase,SendPlan,LoadSender,LoadReceiverand the STOP exchange.crates/udpstp-runtime/src/server.rsandclient.rs: the Tokio Control Phase loops.crates/udpstp-runtime/src/engine.rs: the portable Data Phase Engine;socket/mod.rs: a Test Connection’s socket, which it drives.crates/udpstp-wire/src/lib.rs, thensetup.rs,activation.rs,load.rs,status.rs,auth.rs: PDU codecs and authentication.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).crates/udpstp-core/tests/harness/: the deterministic simulator the protocol tests run on.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
| Area | Libraries and purpose |
|---|---|
| Wire format and authentication | RustCrypto hmac and sha2, with independent known-answer vectors. zeroize wipes key material. ADR 0001 fixes the KDF layout. |
| Protocol core | Deterministic state machines with no I/O. Time, received PDUs and transport outcomes arrive as inputs. |
| Control Phase | Tokio drives sockets and second-scale deadlines. Precise Data Phase pacing stays outside Tokio timers under ADR 0003. |
| Data Phase | Dedicated engine execution, Send Plans and bounded catch-up under ADRs 0008, 0011 and 0012. |
| Socket interfaces | socket2, nix and rustix provide supported operating-system APIs. quinn-udp is restricted to the Windows Standard Port listener under ADR 0007. |
| Application | clap handles arguments and help. Serde and TOML handle configuration and structured data. Tracing supplies logs. |
| Windows service | windows-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.
| Command | Purpose |
|---|---|
cargo xtask req | Validate RFC Requirement IDs, classifications and proving tests, and update conformance output. |
cargo xtask links | Check relative documentation links and anchors. |
cargo xtask glossary | Check domain terms and documented exceptions. |
cargo xtask site prepare | Stage the documentation, generated reference and website sources. |
cargo xtask site finish | Finish 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
ppollon 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_datagramswrites headers (and random payload when requested) into a runtime-owned buffer of twice the largest Burst;report_sentis 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_burstkeeps ahorizonargument 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 @resourcesdeniessched_setaffinitywith 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
ppolltimeout, and keeping arrivals from ending a wait during the hold-off would needepoll_ctlcalls or a nested epoll set. - The epoll wait’s own timeout.
epoll_waittakes 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 needsunsafein the safe wrappers, and its system calls are outside the unit’s filter;sendmmsgtakes 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.rsandactivation_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-yoption. OB-UDPST instead falls back to keyId 0 (DEF_KEY_ID,udpst.cread_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_burstreturns late Bursts one per call, oldest first, each with its scheduled time. A plan holds Deferred Datagrams plus at most one new Burst.horizonremains 0 andreport_sentremains 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’sMessage). The OB-UDPST part tracks OB-UDPST v9.1.0’s schema, except the OB-UDPST Differences recorded indocs/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-configon 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);truncateshows 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;messageis 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):
| ErrorStatus | Meaning | Exit status |
|---|---|---|
| 0 | The Test completed | 0 |
| 2 to 6 | It 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 PDUs | 0 |
| 49 | Ours: it completed without a valid Maximum IP-Layer Capacity; the most severe warning | 0 |
| 50 | The configuration or command line is invalid | 50 |
| 51 | The key file can’t be read or is invalid | 51 |
| 75 | The Client or Server couldn’t start | 75 |
| 100 + cmdResponse | The 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 window | the same |
| 150 + cmdResponse | The same for the Activation exchange: 152 BADPARAM, 150 an undefined code, 157 or 158 | the same |
| 200 | The Test didn’t complete: no answer in time, or the Watchdog ended it | 200 |
| 201 | Ours: the Server coerced a parameter the Client was told to refuse | 201 |
| 202 | Ours: the Server set a parameter the Client can’t run with | 202 |
| 203 | Ours: a Multi-Connection Test’s Servers gave different parameters | 203 |
| 204 | Ours: 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 ErrorStatus | 204, or the signal’s |
| 255 | Any other error | 255 |
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
--helpand 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.tomlandserver-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 innames.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.jsonandextension.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.
- CLI options, value names, spellings and defaults, from clap’s Command model:
- 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
--checksumtakes 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-varnames 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 itsdefine_windows_service!macro. That macro, the documented way to write a service’s entry point, expands anunsafeblock into the calling crate, where#![forbid(unsafe_code)]doesn’t see it (rustc doesn’t report theunsafe_codelint in another crate’s macro), so it would be udpstp’s ownunsafein all but name, which needs an ADR of its own (the specification). The entry point is written by hand instead, a safeextern "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.tomlbans 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, reasonServiceStoporServiceShutdown). 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 onlySeChangeNotifyPrivilege, which every process keeps; a service otherwise holdsSeImpersonatePrivilege, 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, throughtracing-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, asStartupErrorandStartupWarning, 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 audpstpsubcommand: the data folder’s ACL and the firewall rule need Win32 calls that areunsafein Rust, or PowerShell anyway. It mirrorscontrib/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
windowscrate directly: the dispatcher with udpstp’s ownunsafe, and an ADR for it, for nothingwindows-servicedoesn’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-serviceandtracing-appenderare 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 eventsStartupErrorandStartupWarningwith the warnings’ names, and the stop reasonsServiceStopandServiceShutdownare 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. GetKernelObjectSecuritycopies 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
- Microsoft: file security and access rights.
- Microsoft: DACLs and ACEs.
- Microsoft: GetKernelObjectSecurity.
- Microsoft: GetTokenInformation.
- Microsoft: GetFinalPathNameByHandleW.
- ADR 0014: The Server runs as a Windows service of its own, under a virtual account, logging to daily files.
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.