SpeedTestClient
SpeedTestClient is a synthetic chain-head benchmark tunnel. It creates normal Waterwall lines, sends framed speed-test
traffic into the next tunnel, validates frames returned by SpeedTestServer, and prints interval and final throughput
reports.
It is for measuring a Waterwall chain or transport path. It does not open sockets by itself, it is not an application proxy, and it is not a packet-line tunnel.
Typical placement
SpeedTestClient must be the first node in its chain. It generates test lines and sends them upstream to the configured
transport stack.
SpeedTestClient -> TcpConnector
SpeedTestClient -> UdpConnector
SpeedTestClient -> TlsClient -> TcpConnector
SpeedTestClient -> EncryptionClient -> TcpConnector
The remote side should end with a matching SpeedTestServer:
TcpListener -> SpeedTestServer
UdpListener -> SpeedTestServer
TcpListener -> TlsServer -> SpeedTestServer
TcpListener -> EncryptionServer -> SpeedTestServer
TCP example
Client side:
[
{
"name": "speedtest-client",
"type": "SpeedTestClient",
"settings": {
"mode": "tcp",
"direction": "bidirectional",
"duration-ms": 10000,
"warmup-ms": 1000,
"report-interval-ms": 1000,
"connection-count": 4,
"payload-size": 131072,
"terminate-on-complete": true
},
"next": "speedtest-out"
},
{
"name": "speedtest-out",
"type": "TcpConnector",
"settings": {
"address": "198.51.100.10",
"port": 9000,
"nodelay": true
}
}
]
Server side:
[
{
"name": "speedtest-listener",
"type": "TcpListener",
"settings": {
"address": "0.0.0.0",
"port": 9000,
"nodelay": true
},
"next": "speedtest-server"
},
{
"name": "speedtest-server",
"type": "SpeedTestServer",
"settings": {
"report-interval-ms": 1000,
"json-summary": true
}
}
]
UDP example
Client side:
[
{
"name": "speedtest-client",
"type": "SpeedTestClient",
"settings": {
"mode": "udp",
"direction": "upload",
"duration-ms": 10000,
"warmup-ms": 1000,
"payload-size": 3800,
"udp-target-bits-per-sec": 10000000,
"terminate-on-complete": true
},
"next": "speedtest-udp-out"
},
{
"name": "speedtest-udp-out",
"type": "UdpConnector",
"settings": {
"address": "198.51.100.10",
"port": 9001
}
}
]
Server side:
[
{
"name": "speedtest-udp-listener",
"type": "UdpListener",
"settings": {
"address": "0.0.0.0",
"port": 9001
},
"next": "speedtest-server"
},
{
"name": "speedtest-server",
"type": "SpeedTestServer"
}
]
Required fields
SpeedTestClient requires a next node. The next node is the path that receives generated test traffic.
settings is optional in practice; if it is absent or empty, the client uses defaults. Add settings when you need a
specific mode, duration, direction, payload size, stream count, pacing rate, or output behavior.
Optional settings
| Field | Default | Description |
|---|---|---|
mode | tcp | Framing mode. Accepted values: tcp, TCP, udp, UDP. |
direction | upload | Test direction. Accepted values: upload, UPLOAD, send, download, DOWNLOAD, receive, bidirectional, both, BOTH. |
duration-ms | 10000 | Measured test duration after warmup. Must be positive. |
warmup-ms | 0 | Warmup duration. Warmup frames are sent and optionally validated but excluded from measured byte totals. Must be non-negative. |
report-interval-ms | 1000 | Interval report cadence. Must be positive. |
start-delay-ms | 50 | Delay before each generated stream is started after tunnel startup. Must be non-negative. |
timeout-ms | warmup-ms + duration-ms + 30000 | Per-stream watchdog. Must fit in 1..INT_MAX milliseconds and be greater than warmup-ms + duration-ms. If the default exceeds that range, supply a valid explicit timeout or shorten the test. |
connection-count | 1 | Number of parallel normal Waterwall lines to create. Must be positive. |
payload-size | TCP: 131072, UDP: 3800 | Data bytes inside each DATA frame, excluding the 48 byte speed-test header. |
target-bits-per-sec | TCP: 0 | Optional pacing rate in bits per second. 0 means send as fast as backpressure allows. |
udp-target-bits-per-sec | UDP: 10000000 | UDP-specific pacing rate in bits per second. |
target-megabits-per-sec | unset | Alternative pacing field in megabits per second. |
verify-payload | false | Generate and verify deterministic payload bytes at both endpoints; enable for correctness tests. |
json-summary | false | Print a compact final JSON-style summary in addition to normal logs. |
terminate-on-complete | true | Exit Waterwall after all streams complete. Exit code is 0 on success and 1 if any stream failed. |
Only one of target-bits-per-sec, udp-target-bits-per-sec, and target-megabits-per-sec may be configured at the
same time. After conversion to bits per second, the rate must be finite, non-negative, and below 2^64.
payload-size must be between 1 and 16777216. In UDP mode it must not exceed 64952, because each Waterwall UDP
payload carries one complete speed-test frame and the speed-test header is 48 bytes.
Protocol flow
Each generated stream uses these frame types:
| Frame | Direction | Purpose |
|---|---|---|
HELLO | Client to server | Announces mode, direction, duration, warmup, report interval, payload size, pacing rate, verification policy, stream count, and stream id. |
ACK | Server to client | Confirms the HELLO. In UDP mode the client waits for this before sending data. |
DATA | Sender to receiver | Carries zero-filled or verification-pattern payload bytes for upload or download. |
END | Sender to receiver | Marks the end of a sender direction. |
REPORT | Server to client | Carries final sender/receiver statistics. |
ERROR | Server to client | Reports a protocol failure. |
The frame header is 48 bytes. payload-size describes only the DATA payload, not the header.
Startup and line ownership
On tunnel start, the client schedules one startup task per connection-count stream. For each stream it:
- chooses a worker using
stream_id % workers_count - creates a normal Waterwall
line_t - initializes
SpeedTestClientline state - sends upstream
Initto the next tunnel - arms a per-stream watchdog using
timeout-ms
Because the client creates these lines, it owns their destruction. When a stream completes or fails, it destroys its own
line after propagating upstream Finish when appropriate.
TCP behavior
In TCP mode, received bytes are accumulated in a stream buffer and decoded as speed-test frames. TCP mode has no default pacing rate, so it sends as fast as the chain and normal socket/backpressure behavior allow unless you configure a target bandwidth.
The client begins sending after downstream Est from the next tunnel. It also respects downstream Pause and Resume
callbacks by pausing and resuming its send task.
UDP behavior
In UDP mode, each Waterwall payload buffer is one complete speed-test frame. The client retransmits HELLO until it sees
an ACK, using the smaller of report-interval-ms and 250 ms as the retry interval. Final upload END frames are sent
three times to reduce the chance that one lost datagram prevents completion.
UDP mode defaults to udp-target-bits-per-sec: 10000000. It reports loss, duplicates, out-of-order packets, validation
errors, and jitter instead of hiding datagram behavior.
Statistics
The receiver side tracks:
- bytes
- packets
- valid packets
- lost packets
- duplicate packets
- out-of-order packets
- validation errors
- jitter estimate
With verify-payload=true, payloads use a deterministic pattern keyed by stream id, sequence number, and direction.
Sequence accounting remains active when payload verification is disabled.
At the end, the client prints per-stream remote reports from the server and an aggregate final summary. With
json-summary: true, it also prints one compact JSON-style aggregate line.
Common mistakes
- Do not put
SpeedTestClientin the middle of a normal user traffic chain; it is a chain head and generates its own lines. - Do not use it without a matching
SpeedTestServer. - Do not expect it to open sockets directly; use
TcpConnector,UdpConnector, or another transport stack after it. - Do not use very large UDP payloads unless the whole path can carry them without fragmentation or loss.
Payload verification and splice
verify-payload on SpeedTestClient defaults to false and selects the policy
for both endpoints through the HELLO flags. Set it to true to generate and check
the deterministic payload pattern. With verification disabled, senders generate
initialized zero-filled bodies and receivers count their lengths without checking
payload bytes. Frame headers, lengths, stream IDs, sequence accounting and final
reports are still processed. valid_packets and payload validation_errors stay
zero when verification is disabled; JSON summaries include verification_enabled.
Both nodes support splice on eligible TCP chains when misc.splice=true.
The receiver caches the 48-byte header, returns immediately for incomplete frames,
and discards unchecked DATA bodies from their private pipes through /dev/null
on Linux without copying the body into userspace. Unavailable sink/splice support
falls back to bounded ordinary reads. Control bodies and verified DATA bodies are
materialized when needed. UDP retains per-datagram handling. Generated output is
ordinary memory; enabling splice does not make payload generation zero-copy.
Each line bounds retained incomplete-frame storage to 2 * (16 MiB + 48) charged
bytes, compacting before refusing excess storage. This is a queue accounting bound,
not a limit on total process or kernel memory. Send tasks respect Pause and yield
after at most 32 frames; pacing uses scheduled tasks rather than blocking sleeps.
Node Metadata
Source-backed metadata:
| Property | Value |
|---|---|
| node flags | kNodeFlagChainHead + kNodeFlagSupportsSplice |
can_have_prev | false |
can_have_next | true |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayerNone |
layer_group_next_node | kNodeLayer4 |
required_padding_left | 0 bytes |