SpeedTestServer
SpeedTestServer is the chain-end peer for SpeedTestClient. It accepts speed-test frames from upstream, validates
upload data, optionally sends download data back downstream, and returns final sender/receiver reports to the client.
It is a normal connection-oriented tunnel. It does not create lines, it does not own listener-created line destruction, and it is not a packet-line tunnel.
Typical placement
SpeedTestServer must be the last node in its chain.
TcpListener -> SpeedTestServer
UdpListener -> SpeedTestServer
TcpListener -> TlsServer -> SpeedTestServer
TcpListener -> EncryptionServer -> SpeedTestServer
The matching client side normally starts with SpeedTestClient:
SpeedTestClient -> TcpConnector
SpeedTestClient -> UdpConnector
SpeedTestClient -> TlsClient -> TcpConnector
SpeedTestClient -> EncryptionClient -> TcpConnector
TCP example
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,
"quiet": false
}
}
]
Matching client side:
[
{
"name": "speedtest-client",
"type": "SpeedTestClient",
"settings": {
"mode": "tcp",
"direction": "bidirectional",
"duration-ms": 10000,
"warmup-ms": 1000,
"connection-count": 4,
"payload-size": 131072
},
"next": "speedtest-out"
},
{
"name": "speedtest-out",
"type": "TcpConnector",
"settings": {
"address": "198.51.100.10",
"port": 9000,
"nodelay": true
}
}
]
UDP example
Server side:
[
{
"name": "speedtest-udp-listener",
"type": "UdpListener",
"settings": {
"address": "0.0.0.0",
"port": 9001
},
"next": "speedtest-server"
},
{
"name": "speedtest-server",
"type": "SpeedTestServer",
"settings": {
"report-interval-ms": 1000
}
}
]
Matching 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
},
"next": "speedtest-udp-out"
},
{
"name": "speedtest-udp-out",
"type": "UdpConnector",
"settings": {
"address": "198.51.100.10",
"port": 9001
}
}
]
Required fields
SpeedTestServer is a chain end. It must not have a next node.
settings is optional. If it is absent or empty, defaults are used.
Optional settings
| Field | Default | Description |
|---|---|---|
report-interval-ms | 1000 | Fallback interval log cadence before a client HELLO is received. After HELLO, the client's requested interval is used for that line. Must be positive. |
json-summary | false | Default JSON-style per-stream summary setting. A client HELLO can also request JSON summaries per line. |
quiet | false | Suppresses runtime logs such as accepted-stream, interval, final summary, and per-stream failure messages. Fatal setup diagnostics may still appear. |
Init and establishment
On upstream Init, the server:
- initializes per-line state
- creates a receive stream buffer for TCP-mode framing
- immediately sends downstream
Esttoward the previous tunnel
That downstream Est is the signal that lets SpeedTestClient start sending its HELLO.
HELLO and ACK
The first client frame must be HELLO. It configures the server's per-line behavior:
tcporudpmode- upload, download, or bidirectional direction
- duration and warmup
- report interval
- payload size
- target bandwidth
- total stream count
- payload verification policy
- stream id
After accepting HELLO, the server sends ACK downstream. In UDP mode, duplicate HELLO frames receive another ACK
so a lost ACK does not leave the client waiting forever.
The server validates that duration-ms, report-interval-ms, payload-size, and total stream count are usable. UDP
payloads must fit the UDP maximum of 64952 payload bytes.
Upload receive path
When upload is enabled, upstream DATA frames are counted. If the client enables verify-payload,
their bodies are also validated against its deterministic pattern.
The server tracks:
- bytes
- packets
- valid packets
- lost packets
- duplicate packets
- out-of-order packets
- validation errors
- jitter estimate
When the client sends upload END, the server finalizes receiver statistics and sends a receiver REPORT downstream.
In UDP mode, duplicate final END frames are ignored after the receiver has already finished.
Download send path
When download is enabled, the server schedules downstream DATA frames after HELLO is accepted. It uses the payload
size, warmup, duration, and optional target bandwidth requested by the client.
At the end of the download interval, the server sends:
- download
END - sender
REPORT
In UDP mode, final END and REPORT frames are repeated three times.
Stream and datagram framing
In TCP mode, incoming bytes are accumulated in a splice_stream_t and decoded as speed-test frames.
In UDP mode, each Waterwall payload buffer must contain exactly one speed-test frame. A first packet carrying a UDP
flagged HELLO switches that line into UDP-frame handling.
Malformed frame headers, unexpected frame types, impossible payload lengths, or invalid protocol order fail the line.
When possible, the server sends an ERROR frame before closing.
Finish behavior
If upstream closes before a complete test, the server destroys only its own per-line state. It does not call
lineDestroy(), because that line was created by the listener or another upstream adapter.
When the server completes or fails a test itself, it sends any required final protocol bytes, destroys its own line
state, and forwards downstream Finish.
Common mistakes
- Do not place any node after
SpeedTestServer; it is the chain end. - Do not use it without a matching
SpeedTestClient. - Do not expect it to accept arbitrary application traffic.
- Do not treat it as a packet-line node. It works with normal connection lines created by listeners or upstream adapters.
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 | kNodeFlagChainEnd + kNodeFlagSupportsSplice |
can_have_prev | true |
can_have_next | false |
layer_group | kNodeLayer4 |
layer_group_prev_node | kNodeLayer4 |
layer_group_next_node | kNodeLayerNone |
required_padding_left | 0 bytes |