Files
irtt-rs/docs/irtt-client.md
T
Nils Andreas Svee d2d5f87e91
Rust CI / cargo fmt (push) Successful in 16s
Rust CI / cargo clippy (push) Successful in 31s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 25s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Successful in 31s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m11s
Rust CI / cargo doc (push) Successful in 27s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Successful in 1m17s
Fuzz Smoke / cargo fuzz smoke (push) Successful in 1m38s
Netem Interop / netem interop (push) Successful in 1m19s
Supply chain / cargo-deny advisories (push) Successful in 19s
Supply chain / cargo-deny bans licenses sources (push) Successful in 18s
Supply chain / actionlint (push) Failing after 3s
Build binaries / Build (arm-musleabi) (push) Successful in 4m50s
Build binaries / Build (aarch64-musl) (push) Successful in 5m5s
Build binaries / Build (arm-musleabihf) (push) Successful in 4m45s
Build binaries / Build (armv7-musleabi) (push) Successful in 4m38s
Build binaries / Build (armv7-musleabihf) (push) Successful in 4m52s
Build binaries / Build (i686-musl) (push) Successful in 4m54s
Build binaries / Build (mips64-muslabi64) (push) Successful in 4m33s
Build binaries / Build (mips-musl) (push) Successful in 4m35s
Build binaries / Build (mips64el-muslabi64) (push) Successful in 4m44s
Build binaries / Build (mipsel-musl) (push) Successful in 4m45s
Build binaries / Build (riscv64gc-musl) (push) Successful in 4m47s
Build binaries / Build (x86_64-musl) (push) Successful in 5m19s
feat(client): filter output by selected event types
2026-10-28 15:28:26 +07:00

21 KiB
Raw Blame History

irtt-client

Name

irtt-client - IRTT-compatible stream client

Synopsis

irtt-client [OPTIONS] [[LABEL=]TARGET[@hmac=KEY]]...

irtt-client --list-columns

Description

Use irtt-client to probe one or more IRTT-compatible servers over UDP. It prints measurement events as they arrive, and a final summary when the run qualifies for one.

Targets

Write targets as [LABEL=]TARGET[@hmac=KEY]. You can add a LABEL= prefix to choose the name shown in output. If you leave it out, the client uses the target string as its label. You can mix labeled and unlabeled targets in the same command.

If you're changing targets during a long run, give them explicit labels. The label identifies the same logical target when you replace its configuration.

irtt-client host-a:2112
irtt-client eu=host-a:2112 us=host-b:2112
irtt-client host-a:2112 eu=host-b:2112
irtt-client --hmac default-secret eu=host-a:2112@hmac=eu-secret public=host-b:2112@hmac=

Targets without @hmac= inherit the global --hmac value. @hmac=KEY overrides it for that target, and @hmac= explicitly disables HMAC for that target. The modifier uses @, not a shell command separator; quote an argument if its key needs shell quoting.

Both the global --hmac KEY and per-target @hmac=KEY accept plain-text keys or hexadecimal keys prefixed with 0x. For example, secret and 0x736563726574 are equivalent.

Target syntax supports these backslash escapes in labels, addresses, and HMAC values: \\ for a literal backslash, \@ for a literal @, \= for a literal =, and \; for a literal semicolon. Escaping @ is necessary in a label or address when it would otherwise begin @hmac=; escaping the first = lets an unlabeled target or a label contain that character. An = in an HMAC value does not need escaping. Positional target arguments may contain a raw comma; stdin target sets have the additional \, escape described below.

Shells process unquoted backslashes and characters such as semicolons before the client sees them. Single-quote a target when it contains parser escapes, for example 'west\=edge=host\@hmac=literal@hmac=a\;b\\c', so the backslashes reach irtt-client unchanged.

The client probes multiple targets concurrently. By default it staggers them across the probe interval. If you want to send one probe to every active target back-to-back once per interval, use --pacing burst. Final summaries follow the command-line target order, regardless of which target finished first or how their labels sort.

Address family

By default, each target uses the first address returned by the system resolver, whether IPv4 or IPv6. Use -4 / --ipv4 to select IPv4 only, or -6 / --ipv6 to select IPv6 only. Hostnames are resolved when each session connects; these flags do not perform an extra discovery lookup. An explicit IP literal from the other family is rejected during preparation.

Use --dual-stack to compare IPv4 and IPv6 paths as separate measurements:

irtt-client -6 host.example
irtt-client --dual-stack cov=uk-cov1.irtt.lochnair.net

For each hostname, the client discovers which address families the system resolver currently returns. IPv4 results create cov/v4, IPv6 results create cov/v6, and both create both targets, in that order. Without an alias, host.example becomes host.example/v4 and/or host.example/v6. Multiple addresses within one family still produce only one target. Explicit IP literals keep their original label and remain single targets.

Each target has its own session, statistics, and failures. Its original hostname is retained: every new session resolves it again and selects the first address matching its family. Families are discovered only when a declaration is added, so a newly appearing family does not create another target automatically. Discovery failure or a successful lookup returning no addresses rejects the whole target set. A missing family is normal and simply creates no target for that family. Final labels must be unique, including generated /v4 and /v6 labels; choose another alias if one collides with an explicit label.

-4, -6, and --dual-stack are mutually exclusive. Expanded targets use ordinary multi-target pacing and each sends its own probes, so dual-stack can double the number of probes per interval compared with one target per hostname.

Local socket configuration

If you need to control where probes leave the machine, use these socket options:

  • --bind-addr IP[:PORT] binds sockets to a local IP address and optional port. Leave out the port or use 0 to let the OS assign an ephemeral port. IPv6 addresses with a port need [ADDR]:PORT syntax. The address family must match the target's resolved address family.
  • --bind-device DEVICE (Linux, Android, Fuchsia) binds sockets to a network interface using SO_BINDTODEVICE.
  • --mark MARK (Linux, Android, Fuchsia) sets the socket's firewall mark using SO_MARK, so policy routing or firewall rules can select the traffic. It accepts a decimal or 0x-prefixed hexadecimal unsigned 32-bit integer.
  • --fib FIB (FreeBSD) selects a routing table using SO_SETFIB. Supply the FIB as a decimal unsigned 32-bit integer.

For example, to bind IPv4 probes to a specific source address and interface with a firewall mark:

irtt-client -4 --bind-addr 192.0.2.10 --bind-device wan0 --mark 1234 host.example

Setting a firewall mark or binding to a network device may require elevated privileges or appropriate capabilities. If the OS rejects a socket option, socket setup fails.

A fixed local port can cause bind conflicts when the client creates sockets for multiple targets. Unless you need a particular source port, let the OS assign one. A bind address for one family also cannot be used with targets in the other family.

Dynamic target sets from standard input

--targets-stdin only works in continuous mode (--continuous or --duration 0). Use it to change targets while the client is running. Positional targets form the initial set. You can leave them out to start with none:

irtt-client --duration 0 --targets-stdin
irtt-client --duration 0 --targets-stdin ams=ams.example:2112

Each non-empty stdin record gives the complete set of targets you want. A new record replaces the previous set: the client retires targets you leave out, keeps the generation for unchanged configurations, and starts a fresh generation when an address or HMAC setting changes. If live-generation capacity temporarily prevents an update, only the latest unapplied set is kept.

The same address-family preparation applies to initial and stdin targets. With --dual-stack, unchanged labels and endpoints retain their discovered families across records, including HMAC-only changes. Removing a declaration and later adding it again performs fresh discovery. New declarations are prepared asynchronously while existing measurements continue. A preparation error stops the stream gracefully without applying a partial set.

Separate targets in stdin records with commas, and escape a comma inside a target as \,. Each target otherwise uses the same syntax as positional arguments, including the escapes above, @hmac=KEY, and @hmac=.

Keep the producer or pipe/FIFO writer open while controlling the client. For example, run this in one terminal:

mkfifo /tmp/irtt-targets
irtt-client --duration 0 --targets-stdin --hmac default-secret \
  < /tmp/irtt-targets

Then, in another terminal, open the FIFO once and keep that descriptor open while sending complete target sets:

exec 3> /tmp/irtt-targets
printf '%s\n' 'ams=ams.example:2112,sg=sg.example:2112@hmac=sg-secret' >&3
printf '%s\n' 'sg=sg.example:2112@hmac=sg-secret,nyc=nyc.example:2112' >&3
# Publish more sets as needed, then send EOF and stop the client:
exec 3>&-

Send exactly [] to select an empty target set. The client ignores empty records, but otherwise treats whitespace as part of the input without trimming it.

Closing stdin sends EOF and asks the client to stop gracefully. An update that has been read but not yet acknowledged as applied may be discarded at that point. EOF does not wait for an update blocked by capacity to become applicable.

The maximum stdin record payload is 64 KiB. Stdin-controlled mode bounds each desired set to 128 targets and retains at most 256 live target generations while replacements drain. The 128-target limit applies after dual-stack expansion: a hostname producing both families consumes two target slots.

Negotiation only

-n / --no-test connects to each target, negotiates the requested parameters, and exits without sending measurement probes. For example:

irtt-client --no-test host.example:2112 --duration 30s --interval 100ms

On successful negotiation, the no_test_completed event includes the negotiated duration, interval, and packet length. Output format and column selection work as usual. The client prints no measurement summary or statistics-memory warning. Negotiation failures use the normal target diagnostics and exit policy: an uninterrupted run exits nonzero if no target succeeds.

Duration options still specify the duration to negotiate; even with --continuous or --duration 0, the client exits after negotiation. --no-test cannot be combined with --targets-stdin.

Finite versus continuous operation

--duration and --interval accept fractional and composite durations, such as 2.5s, 1m30s, and 1h, as well as ms, us (or µs), and ns units (e.g. 500us or 100ns). A nonzero value requires a unit. The interval must be greater than zero; a zero test duration, including 0s, selects continuous mode.

--timeouts 1s,2s,4s,8s sets the session-opening retry schedule. Each comma-separated duration is the receive timeout for one open attempt; attempts stop when a valid open reply arrives. The list must be nonempty, with each value at least 200ms, and uses the same duration syntax as --interval. Omitting the option preserves the library default schedule.

--duration controls the run length:

  • A nonzero duration (default 10s) gives you a finite run. Probing stops after that long. If statistics are enabled and you're using table output, the client prints a final summary. CSV, TSV, and JSON Lines never include a summary.
  • With --continuous (or --duration 0), the client runs until interrupted (Ctrl-C). With statistics and table output enabled, interrupting the run prints a final summary. In stdin-controlled mode, EOF also stops the run gracefully and prints the retained summary. A continuous run that exits for another reason without being interrupted does not print one.

The choice also affects which statistics are available and how much memory a long run retains. See Measurements and memory.

Optional recovery and event-only runs

If you only need individual measurements, use --no-stats to disable statistics collection and final summaries. Columns that need statistics, such as IPDV, are unavailable. The runtime flag still leaves the irtt-stats dependency in the build.

To omit statistics at compile time, build with:

cargo build --release -p irtt-rs --bin irtt-client --no-default-features --features client

Use --features client,stats to include statistics in a client-only build. Default builds include statistics, and tui always enables them. Without compiled statistics, --no-stats remains accepted, summaries and statistics warnings are disabled, and IPDV columns retain their names with absent values (- in tables, empty CSV/TSV fields, and JSON null). Measurement events and protocol-level received statistics remain available.

Use --reconnect to recover individual targets in continuous mode (--continuous or --duration 0). The client retries failed connections, closed sessions, and unresponsive servers while the other targets keep running. By default, the delay doubles after each failed attempt, from 250 ms up to 10 seconds. A valid reply resets it to the initial delay. Each target backs off independently.

You can change the delays with --reconnect-initial-delay DURATION (default: 250ms) and --reconnect-max-delay DURATION (default: 10s). Both require --reconnect and must be positive and schedulable. The maximum must be at least twice the initial delay. For example, --reconnect-initial-delay 1s --reconnect-max-delay 5s retries after 1s, 2s, 4s, 5s, then 5s from there on.

--reconnect-timeout DURATION sets how long a session can go without valid replies before the client tries to reconnect (default: 10s). It only applies while actively sending probes, so allow for your probe interval and expected packet loss.

Reconnecting resets statistics for that target, so its final summary only covers the latest session. The client warns about this when reconnection and statistics collection are both enabled. Use --no-stats to disable collection and suppress the warning.

For continuous CSV output from multiple targets without statistics:

irtt-client --continuous --reconnect --no-stats \
  --reconnect-timeout 10s \
  --reconnect-initial-delay 250ms --reconnect-max-delay 10s \
  --format csv --header never \
  --columns client_receive_wall_ns,target,seq,rd_us,sd_us \
  reflector1 reflector2

Output

--format FORMAT

Choose one of four event-row formats. The default is table:

  • table: human-readable terminal output; the only format that prints a final summary.
  • csv: comma-separated output.
  • tsv: tab-separated output.
  • jsonl: one JSON object per event.

CSV, TSV, and JSON Lines include all columns by default for structured export; table defaults to a compact column set and hides echo_sent events. You can show those events by explicitly selecting them with --events or by using custom columns.

--events EVENTS

Choose which events to print with a comma-separated list, or repeat the option to select more events. The available names are session_started, no_test_completed, session_closed, echo_sent, echo_reply, loss, duplicate, late, and warning. Without this option, the client prints all events normally shown by the selected format.

For CSV output containing only echo replies:

irtt-client <server> --events echo_reply --format csv --header never --no-stats

--columns COLUMNS / -c COLUMNS

Choose event-row columns with a comma-separated list, or use default for the format's default set and all for every column. To see the available columns, their meanings, and aliases, run:

irtt-client --list-columns

You don't need a target for --list-columns.

--header auto|always|never

Header policy for table, csv, and tsv output (default auto). JSON Lines never prints a header line.

--verbose

Include extra fields in table output and final summaries. CSV, TSV, and JSON Lines keep their default all-column set or your explicit --columns selection regardless of --verbose.

Examples

irtt-client <server> --format table
irtt-client <server> --format jsonl
irtt-client <server> --format csv --columns event,seq,remote,effective_rtt_us

To print only effective RTT values in microseconds:

irtt-client <server> --format tsv --columns effective_rtt_us --header never

Measurement fields

Useful measurement columns:

  • raw_rtt_us: client-observed send-to-receive RTT. Always userspace-timed and unaffected by kernel timestamp preference (below).
  • adjusted_rtt_us: RTT adjusted for server processing when available. Can be negative when server processing exceeds the measured raw RTT.
  • effective_rtt_us: adjusted_rtt_us when available, otherwise raw_rtt_us.
  • rd_us / sd_us (selectable with the longer aliases receive_delay_us / send_delay_us, but always emitted under the short names): one-way delay estimates. Can be negative because of clock skew between client and server.
  • ipdv_us: inter-packet delay variation.
  • server_processing_us: time spent processing the packet at the server.
  • kernel_rx_ns: kernel receive timestamp, in Unix nanoseconds, when captured (see below).

Kernel timestamps (Linux)

On Linux, the client prefers kernel timestamps for its own send/receive endpoint when calculating one-way delays, where the platform and socket support them. This reduces the effect of scheduling and queuing delay. The irtt-server applet makes the same choice for its endpoint on Linux. With another IRTT-compatible server, the client uses the wall-clock value that server reports as-is.

If a kernel timestamp is unsupported or implausible, timing falls back to userspace. This leaves raw_rtt_us unchanged. The adjusted_rtt_us/effective_rtt_us values can still shift slightly, since server processing time uses the same server receive endpoint.

Measurements and memory

If the stats feature is compiled in and you haven't used --no-stats, the client collects statistics as described here. Disabling collection allocates no statistics collectors, though the client still keeps its bounded reply-classification state.

In finite mode, the client keeps exact timing samples and adjacent-sequence IPDV state so it can report exact medians in the final summary. Memory grows with the number of probes sent, at roughly a few hundred bytes per probe per target before allocator overhead. So if you're planning a long finite run with a short interval, allow for that growth.

The client prints a warning on stderr when its estimate of retained state crosses roughly 128 MiB, with stronger warnings past roughly 512 MiB and 1 GiB. These are useful planning figures, but they don't promise an RSS ceiling: allocator and OS overhead are outside the estimate.

In continuous mode (--duration 0), running statistics keep only count/min/max/mean/variance, so exact medians are unavailable. The client still keeps up to 4,096 sequences per target for adjacent-sequence IPDV pairs. Statistics memory approaches a fixed limit per target as the run continues.

Reply classification has its own bounds, regardless of whether you collect statistics. Each target has separate pending, timed-out, and completed/duplicate sequence stores, each with a 4,096-entry limit. If the pending store fills, the client fails and drains that target as resource-exhausted. It does not wait for space to free up.

The timed-out and completed stores evict their oldest entries. A very late reply can still be seen and counted after eviction, but without its retained send state the client cannot calculate measurements such as RTT.

In continuous mode, retained state scales with the number of configured targets and these fixed limits per target. The client writes output as events arrive. The managed event stream has a 16,384-event capacity and can drop events if the consumer falls behind. If that happens, the client reports that output and statistics may be incomplete.

For capacity planning on constrained devices (for example an OpenWrt router): a finite run's statistics memory is roughly proportional to targets × probes_sent, while a continuous run's statistics memory approaches a fixed multiple of targets regardless of how long it runs.

Peer close and exit behavior

If a peer closes a finite run, the client treats that as successful completion. In continuous mode without --reconnect, a peer closure ends an uninterrupted run with a nonzero exit status so a process supervisor can restart it. If you interrupted the run (Ctrl-C), peer closure is never reported as an error in either mode.

With --reconnect, a desired target reopens automatically after peer closure. Without recovery, --targets-stdin makes peer closure target-local: other desired targets and the stdin controller continue running. Including that target in a later complete set starts a fresh generation.

Exit status

The client exits 0 on success, including normal finite completion and interrupted runs in either mode. It exits nonzero if the managed driver fails, an uninterrupted continuous run ends by peer closure, or no target completes successfully in an uninterrupted run.

Interrupting the run prevents the last condition from counting as a failure. For example, if you interrupt a continuous run whose only target never opened, it still exits 0.

Server fill request

--sfill DESCRIPTOR requests the server's echo-reply payload fill: none (zero-filled), rand (random bytes), or pattern:HEX (a repeating hexadecimal pattern). See the Server fill section in irtt-server(1) for what the server does with an unset or unparseable request.

Examples

irtt-client netperf-eu.bufferbloat.net:2112
irtt-client netperf-eu.bufferbloat.net:2112 --duration 30s --interval 100ms
irtt-client netperf-eu.bufferbloat.net:2112 --duration 0
irtt-client host-a:2112 host-b:2112 --pacing burst

See also

irtt-tui(1), irtt-server(1), irtt-rs(1), and irtt-client --help for the full option list, including HMAC, clock/timestamp negotiation, DSCP, and TTL flags.