Skip to main content

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​

FieldDefaultDescription
modetcpFraming mode. Accepted values: tcp, TCP, udp, UDP.
directionuploadTest direction. Accepted values: upload, UPLOAD, send, download, DOWNLOAD, receive, bidirectional, both, BOTH.
duration-ms10000Measured test duration after warmup. Must be positive.
warmup-ms0Warmup duration. Warmup frames are sent and optionally validated but excluded from measured byte totals. Must be non-negative.
report-interval-ms1000Interval report cadence. Must be positive.
start-delay-ms50Delay before each generated stream is started after tunnel startup. Must be non-negative.
timeout-mswarmup-ms + duration-ms + 30000Per-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-count1Number of parallel normal Waterwall lines to create. Must be positive.
payload-sizeTCP: 131072, UDP: 3800Data bytes inside each DATA frame, excluding the 48 byte speed-test header.
target-bits-per-secTCP: 0Optional pacing rate in bits per second. 0 means send as fast as backpressure allows.
udp-target-bits-per-secUDP: 10000000UDP-specific pacing rate in bits per second.
target-megabits-per-secunsetAlternative pacing field in megabits per second.
verify-payloadfalseGenerate and verify deterministic payload bytes at both endpoints; enable for correctness tests.
json-summaryfalsePrint a compact final JSON-style summary in addition to normal logs.
terminate-on-completetrueExit 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:

FrameDirectionPurpose
HELLOClient to serverAnnounces mode, direction, duration, warmup, report interval, payload size, pacing rate, verification policy, stream count, and stream id.
ACKServer to clientConfirms the HELLO. In UDP mode the client waits for this before sending data.
DATASender to receiverCarries zero-filled or verification-pattern payload bytes for upload or download.
ENDSender to receiverMarks the end of a sender direction.
REPORTServer to clientCarries final sender/receiver statistics.
ERRORServer to clientReports 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:

  1. chooses a worker using stream_id % workers_count
  2. creates a normal Waterwall line_t
  3. initializes SpeedTestClient line state
  4. sends upstream Init to the next tunnel
  5. 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 SpeedTestClient in 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:

PropertyValue
node flagskNodeFlagChainHead + kNodeFlagSupportsSplice
can_have_prevfalse
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayerNone
layer_group_next_nodekNodeLayer4
required_padding_left0 bytes