docs: turn the root README into a project landing page

Reduce the root README to project discovery and navigation: what
irtt-rs is, its relationship to upstream IRTT (including a factual
capability comparison checked against upstream v0.9.2), installation,
a short quick start, and links out to detailed documentation. Detailed
CLI behavior, output/measurement semantics, continuous-mode memory
guidance, and server bind/session/negotiation semantics move into new
manpage-shaped docs under docs/ (irtt-rs, irtt-client, irtt-tui,
irtt-server), rewritten as operational guidance rather than copied
verbatim from the old README sections they replace.
This commit is contained in:
Nils Andreas Svee committed 2026-08-20 22:17:25 +07:00
1 parent e9e1ff4218
commit 1ec82d8c56
5 files changed
+743 -343

No files matched your search

+224
View File
@@ -0,0 +1,224 @@
# irtt-client
## NAME
irtt-client - IRTT-compatible stream client
## SYNOPSIS
`irtt-client` [*OPTIONS*] [*[LABEL=]TARGET*]...
`irtt-client` `--list-columns`
## DESCRIPTION
`irtt-client` probes one or more IRTT-compatible servers over UDP and prints
per-probe measurement events as they arrive, plus a final summary for
eligible runs.
## TARGETS
Every target argument accepts `[LABEL=]TARGET`. An optional `LABEL=` prefix
assigns the logical target name used in output; without it, the target
string itself is used as the label. Labeled and unlabeled targets can be
freely mixed in one argument list.
```sh
irtt-client host-a:2112
irtt-client eu=host-a:2112 us=host-b:2112
irtt-client host-a:2112 eu=host-b:2112
```
Multiple targets are probed concurrently. By default, a multi-target session
uses **staggered** pacing, spacing active targets across the probe interval;
pass `--pacing burst` to send one probe to every active target back-to-back
once per interval instead. Final per-target summaries print in the order
targets were supplied on the command line, not the order in which they
finish or their labels sort alphabetically.
## FINITE VERSUS CONTINUOUS OPERATION
`--duration` controls the run length:
- Any nonzero duration (default `10s`) is a **finite** run: probing stops
after that long, and a final summary is always printed for table output
(see [OUTPUT](#output) below — CSV, TSV, and JSON Lines never print one).
- `--duration 0` is **continuous**: the client runs until interrupted
(`Ctrl-C`). For table output, a final summary is printed only if the run
was interrupted; an uninterrupted continuous run (one that exits for
another reason) does not get one.
See [MEASUREMENTS AND MEMORY](#measurements-and-memory) below for why this
distinction also determines what statistics can be computed and how much
memory a long run retains.
## OUTPUT
### `--format FORMAT`
One of four event-row formats, default `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 default to **all** columns for structured export;
`table` defaults to a compact column set and hides `echo_sent` rows. An
explicit `--columns` selection other than `default` shows all event rows,
including `echo_sent`; `--columns default` is equivalent to omitting the
option and keeps `echo_sent` hidden for table output.
### `--columns COLUMNS` / `-c COLUMNS`
Comma-separated event row columns, or the special values `default` (the
format's default set) or `all` (every column). List every available column,
its meaning, and its aliases with:
```sh
irtt-client --list-columns
```
(`--list-columns` does not require a target.)
### `--header auto|always|never`
Header policy for `table`, `csv`, and `tsv` output (default `auto`). JSON
Lines never prints a header line.
### `--verbose`
Includes extra fields in table output and final summaries. Has no effect on
CSV, TSV, or JSON Lines output either way — those formats show their default
all-column set or an explicit `--columns` selection exactly as given,
regardless of `--verbose`.
### Examples
```sh
irtt-client <server> --format table
irtt-client <server> --format jsonl
irtt-client <server> --format csv --columns event,seq,remote,effective_rtt_us
```
A stream containing only effective RTT values in microseconds:
```sh
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, one-way delay estimates prefer a kernel-captured timestamp over
userspace timing for this client's own send/receive endpoint, where the
platform and socket support it, reducing the effect of scheduling and
queuing delay. The peer's reported endpoint gets the same preference when it
is the `irtt-server` applet running on Linux; against another
IRTT-compatible server, whatever wall-clock value it reports is used as-is.
This is best-effort: an unsupported or implausible kernel timestamp falls
back to userspace timing. `raw_rtt_us` is unaffected either way;
`adjusted_rtt_us`/`effective_rtt_us` can still shift slightly, since server
processing time is derived from the same server receive endpoint.
## MEASUREMENTS AND MEMORY
Finite and continuous modes make a different tradeoff between exact
statistics and bounded memory.
**Finite mode** retains exact timing samples (and adjacent-sequence IPDV
state) so the final summary can report exact medians. Retained memory
therefore grows with the number of probes sent, at a small constant cost per
probe (on the order of a few hundred bytes of retained sample state per
probe per target, before allocator overhead) — a very long finite run at a
short interval is the case to plan capacity for. The client warns on stderr
when its own estimate of this retained state crosses roughly 128 MiB, and
again more strongly past roughly 512 MiB and 1 GiB; treat those numbers as
practical guidance, not a promised RSS ceiling, since allocator and OS
overhead sit outside the estimate.
**Continuous mode** (`--duration 0`) does not retain a growing sample
history: running statistics keep count/min/max/mean/variance only, so exact
medians are unavailable. It still keeps a bounded adjacent-sequence IPDV
store, capped at 4,096 sequences per target, so nearby replies can still
form IPDV pairs. Retained statistics state therefore approaches a fixed
per-target bound rather than growing with elapsed run time.
Reply classification (independent of the statistics choice above) is also
bounded per target: each of the pending, timed-out, and completed/duplicate
sequence stores has its own 4,096-entry limit — not one shared total across
the three. A full pending store makes the client fail and drain that target
as resource-exhausted rather than wait for capacity to free. Timed-out and
completed state evict their oldest entries, so a sufficiently old late reply
can still be seen and counted, but no longer has retained send state for
measurements such as RTT.
Overall, retained state scales with the **number of configured targets**
and these fixed per-target limits, not with total probes sent in continuous
mode. Output is written as events arrive rather than accumulated; the
managed event stream feeding it is itself bounded and lossy (16,384 events),
so a sufficiently slow consumer can miss events, in which case the client
reports that resulting output/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
A peer closing a finite run is accepted as a terminal, successful outcome.
In continuous mode, an uninterrupted peer closure is treated as an error —
the client exits nonzero — so a process supervisor can restart it; an
interrupted run (`Ctrl-C`) never reports peer closure as an error, whichever
mode it was in.
## EXIT STATUS
`0` on success (including a finite run's ordinary completion, and an
interrupted run of either mode). Nonzero if the managed driver failed, if an
uninterrupted continuous run ended by peer closure, or if the run was not
interrupted and no target completed successfully. An interrupted run never
fails on the last of these conditions — for example, interrupting a
continuous run whose only target never opened 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 `irtt-server(1)` SERVER FILL for what the server
does with an unset or unparseable request.
## EXAMPLES
```sh
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.
+64
View File
@@ -0,0 +1,64 @@
# irtt-rs
## NAME
irtt-rs - IRTT-compatible multi-applet dispatcher
## SYNOPSIS
`irtt-rs` `client` [*OPTIONS*] [*[LABEL=]TARGET*]...
`irtt-rs` `tui` [*OPTIONS*] *[LABEL=]TARGET*...
`irtt-rs` `server` [*OPTIONS*]
## DESCRIPTION
`irtt-rs` is a multicall dispatcher: one binary that runs the client, TUI,
or server applet depending on the subcommand given, or on the name it was
invoked/copied as. It exists for router-friendly single-binary deployment;
it does not add behavior beyond what `irtt-client(1)`, `irtt-tui(1)`, and
`irtt-server(1)` already provide.
Each subcommand behaves exactly like its dedicated binary — `irtt-rs client
...` is equivalent to running `irtt-client ...` directly. See those manuals
for target syntax, options, output, and exit behavior.
## APPLET BINARY NAMES
The build also produces dedicated binaries: `irtt-client`, `irtt-tui`, and
`irtt-server`. A dedicated binary always runs its own role, regardless of
what it is invoked or copied as — it does not consult argv0 dispatch logic.
`irtt-rs` itself inspects argv0 first: if invoked (or symlinked/copied) under
one of the recognized applet names — `irtt-client`, `irtt-tui`, `irttd`, or
`irtt-server` — it runs that applet directly, without needing a subcommand.
Any other name starting with `irtt-` (other than `irtt-rs` itself) is
rejected as an unknown applet name before subcommand parsing is even
attempted — `irtt-custom server` is a hard error, not a fallthrough. Only
`irtt-rs` itself, or a name that doesn't start with `irtt-` at all, falls
through to ordinary `client`/`tui`/`server` subcommand parsing.
## FEATURE-DEPENDENT APPLET AVAILABILITY
Each applet is gated by a Cargo feature on the `irtt-rs` package: `client`,
`tui`, `server`. All three are in the default feature set. A build with a
feature disabled still produces the `irtt-rs` binary, but that applet is
unavailable at runtime — running it (by subcommand or by binary name)
reports which feature to rebuild with, and `irtt-rs --help` lists which
applets the current build actually contains.
```sh
cargo install irtt-rs --locked --no-default-features --features client
```
## RELATIONSHIP TO irtt-client, irtt-tui, irtt-server
`irtt-rs` is packaging and dispatch only. All probing, display, and server
behavior lives in `irtt-client(1)`, `irtt-tui(1)`, and `irtt-server(1)` —
consult those manuals for anything observable about running a probe or
serving one.
## SEE ALSO
`irtt-client(1)`, `irtt-tui(1)`, `irtt-server(1)`.
+261
View File
@@ -0,0 +1,261 @@
# irtt-server
## NAME
irtt-server - IRTT-compatible UDP server
## SYNOPSIS
`irtt-server` [`--bind` *ADDR*]... [*OPTIONS*]
`irtt-rs server` [`--bind` *ADDR*]... [*OPTIONS*]
## DESCRIPTION
`irtt-server` answers IRTT echo requests over UDP. It is thin orchestration
over the reusable `irtt-server` library: one current-thread Tokio runtime,
one or more listeners, and the library's own session/policy logic.
Run with no arguments for the ordinary case:
```sh
irtt-server
```
With no `--bind`, the server listens on the standard IRTT port (2112) on the
wildcard address for both address families, in this fixed order:
1. `[::]:2112`
2. `0.0.0.0:2112`
This is available wherever the platform supports safe wildcard reply-source
selection (see [Wildcard binds](#wildcard-binds) below). Any explicit
`--bind` **replaces** this default pair entirely rather than adding to it —
`irtt-server --bind 127.0.0.1:2112` binds only that one address.
If one of the two default addresses cannot be bound because its address
family itself has no local support at all — IPv6 disabled on an otherwise
IPv4-capable host, for example — the server falls back to serving just the
other one instead of failing outright, and says so on startup. Any other
reason a default listener fails — the port already in use, permission
denied, no safe wildcard reply-source path — is not a family-support
problem and is not silently worked around; the invocation fails as it would
for an explicit `--bind`. This fallback applies only to the implicit default
pair: an explicit `--bind` list always keeps the ordinary all-or-nothing
behavior described below.
## OPTIONS
### `--bind ADDR`
Local UDP address to bind, as `ADDR:PORT`, for example `127.0.0.1:2112` or
`[::1]:2112`. Host names are not resolved. Repeatable.
Repeat the option to serve several addresses from one process, in the order
given. Every listener applies the same policy options, but each keeps its
own sessions and tokens, so a session belongs to the address it was opened
on. Binding is all-or-nothing: if any address cannot be bound, none are
served.
A port of `0` selects an unused port per listener, so two such binds get two
different ports.
With no `--bind` at all, see [Default bind](#default-bind) above.
### `--hmac KEY`
HMAC key, taken as the UTF-8 bytes of this argument. With a key configured,
every request must carry a valid MAC and every reply is authenticated;
without one, authenticated requests are dropped. The key is visible in the
process arguments.
### `--max-sessions COUNT`
Maximum number of simultaneously live sessions **per listener**, not per
process. Once a listener's table is full, a session-creating open to it is
dropped silently; nothing is evicted. Zero refuses every session-creating
open.
### `--max-packet-length BYTES`
Maximum echo datagram size a session may negotiate, in bytes. A longer
requested length is reduced during negotiation, an open whose mandatory
field block would not fit is refused, and inbound echo requests are admitted
against the same limit. This is a resource bound, not an MTU.
### `--min-interval DURATION`
Floor on the send interval a session may negotiate. The negotiated interval
is still capped at a quarter of the idle timeout afterwards, so a
`--min-interval` above that cap is not the value actually negotiated.
A session's reply allowance refills at the shorter of this configured floor
and the interval it actually negotiated. Ordinarily that is the configured
floor itself, since the negotiated interval is normally at least as long —
a 10ms `--min-interval` against a 1s negotiated interval refills every
10ms. The two differ only when the idle-timeout cap above pulled the
negotiated interval below the configured floor, in which case the shorter,
negotiated value is what the allowance actually refills at: a server must
not enforce a cadence it did not advertise to the client. Use `0` for no
time-based throttling.
### `--burst COUNT`
Echo replies one session may have answered before its allowance has to
refill. Use `0` for no allowance at all, which rate-limits every echo
request whatever the interval is.
### `--idle-timeout DURATION`
Release a session after this long without a served or rate-limited echo
request. The deadline runs from the open, and release is silent. Use `0` to
expire a session at the next evaluation; it is not a way to disable expiry.
### `--max-duration DURATION`
Maximum test duration a session may negotiate. A longer request is reduced
to it, and a continuous request is answered with it. Omit this flag for no
maximum; a maximum of zero cannot be expressed, because a negotiated
duration of zero means continuous.
### `--timestamp-allowance MODE`
How many timestamps this server will provide: `dual` (default, honors a
request for send, receive, both, or midpoint timestamps), `single`
(provides at most one timestamp instant; a request for both is negotiated to
midpoint), or `none` (no timestamps at all). The requested clock source is
never changed, only which instants are reported, so a single instant is
still reported once per requested clock.
### `--no-dscp`
Refuse to provide requested traffic-class marking. Any requested DSCP is
negotiated to zero, so the client is told its echo replies will be unmarked,
and they are sent unmarked. The session is not refused.
Durations everywhere in this command accept an integer followed by a unit:
`ms`, `s`, or `m`.
## OUTPUT
Each bound endpoint is printed on startup, once every listener is up, which
also resolves a port of `0`:
```
irtt-server: listening on [::]:2112
irtt-server: listening on 0.0.0.0:2112
```
`Ctrl-C` stops the server gracefully.
## MULTIPLE LISTENERS
Every invocation runs through the same multi-listener path, so one address —
whether from the default pair or a single explicit `--bind` — is an ordinary
set of one rather than a separate mode. The policy options above apply to
every listener, but listeners are otherwise independent: each has its own
sessions and tokens, and `--max-sessions` bounds each listener rather than
the process. If any listener fails while running, the others are shut down
with it rather than leaving a service configured for two families answering
on one.
Wildcard IPv4 and IPv6 listeners may share one port, as the default pair
does.
## SERVER FILL
Echo replies fill their payload according to the negotiated `ServerFill`
mode, which a client requests with `irtt-client`'s `--sfill` (see
`irtt-client(1)`):
- `none`: zero-filled.
- `rand`: random bytes.
- `pattern:HEX`: the given repeating hexadecimal pattern.
Every valid descriptor is honored. A descriptor the server cannot parse, and
a client that expresses no fill preference at all, both get the default
`pattern:69727474`, i.e. the repeating bytes `69 72 74 74` (`irtt`).
## WILDCARD BINDS
An explicit interface address works on every supported system. A wildcard
bind such as `0.0.0.0:2112` or `[::]:2112` reads each request's local
destination from packet metadata and sends that request's reply from the
same address, so a client on a multi-homed host is answered from the
endpoint it actually contacted.
That reply-source selection is implemented on **Linux, macOS, and FreeBSD**.
Elsewhere, a wildcard bind is refused at construction rather than served
from a routing-table-selected source address a client would discard — this
also applies to the zero-argument default, which will fail with a clear
error asking for an explicit `--bind` on such a platform. This correctness
rule is deliberate and is not weakened by the zero-argument default.
## EXAMPLES
Run with the default wildcard listeners on the standard port:
```sh
irtt-server
```
Bind a single explicit address:
```sh
irtt-server --bind 127.0.0.1:2112
```
Through the `irtt-rs` dispatcher:
```sh
irtt-rs server --bind 192.0.2.10:2112
```
Serve two explicit addresses from one process:
```sh
irtt-server \
--bind 0.0.0.0:2112 \
--bind [::]:2112
```
Apply session policy:
```sh
irtt-server \
--bind 192.0.2.10:2112 \
--max-sessions 512 \
--idle-timeout 30s
```
Restrict optional capabilities:
```sh
irtt-server \
--bind 192.0.2.10:2112 \
--timestamp-allowance single \
--no-dscp
```
Require authentication:
```sh
irtt-server --bind 192.0.2.10:2112 --hmac secret
```
## EXIT STATUS
`0` on a clean shutdown (`Ctrl-C`), nonzero if binding fails or a listener
fails while running.
## LIMITATIONS
- No hostname resolution, interface expansion, daemonization, or
configuration file — binds are explicit addresses only.
- Wildcard reply-source selection is unavailable outside Linux, macOS, and
FreeBSD; such platforms must bind explicit addresses.
## SEE ALSO
`irtt-client(1)`, `irtt-tui(1)`, `irtt-rs(1)`, and `irtt-server --help` for
the full option list.
+83
View File
@@ -0,0 +1,83 @@
# irtt-tui
## NAME
irtt-tui - IRTT-compatible terminal UI client
## SYNOPSIS
`irtt-tui` [*OPTIONS*] *[LABEL=]TARGET*...
## DESCRIPTION
`irtt-tui` is a live dashboard over the same probing engine as
`irtt-client(1)`: a graph and status view instead of a printed event stream.
At least one target is required.
Target syntax, multiple targets, and pacing are identical to
`irtt-client(1)` — see that manual for `[LABEL=]TARGET` syntax and the
`--pacing staggered|burst` option. The full set of negotiation flags
(`--interval`, `--length`, `--hmac`, `--clock`, `--tstamp`, `--stats`,
`--sfill`, `--dscp`, `--ttl`, `--loose`) is shared with `irtt-client` as
well; consult that manual for their meaning.
## CONTINUOUS DEFAULT
Unlike `irtt-client`, **the TUI defaults to continuous mode**
(`--duration 0`): it runs until you quit. Pass an explicit `--duration` for
a finite run:
```sh
irtt-tui <server> --duration 30s
```
## CONTROLS
| Key | Action |
| --- | --- |
| `q`, `Ctrl-C` | Quit |
| `p` | Toggle display pause (probing continues; only redraws stop) |
| `g` | Toggle Graph / Dashboard view |
| `m` | Cycle graph metric |
| `r` | Clear visible graph history |
| `←` / `→` | Pan graph |
| `PageUp` / `PageDown` | Page-pan graph |
| `Home` / `End` | Jump to oldest / jump to live |
| `+` / `=` | Zoom graph in |
| `-` | Zoom graph out |
| `0` | Reset graph window and zoom |
## RETAINED HISTORY
The TUI keeps its own bounded presentation state on top of the statistics
retention described in `irtt-client(1)`:
- Up to 100,000 graph samples per target.
- Up to 80 recent status/log messages.
`r` clears the currently visible graph history and returns the viewport to
live; it does not change the 100,000-sample cap itself. These are
presentation bounds on top of the client's own statistics retention (see
`irtt-client(1)` MEASUREMENTS AND MEMORY) — a long continuous TUI session's
total memory is dominated by whichever of the two is larger for your target
count.
## EXIT BEHAVIOR
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. Otherwise,
exit status follows the same peer-close and driver-failure rules as
`irtt-client(1)`.
## EXAMPLES
```sh
irtt-tui host.example
irtt-tui eu=host.example
irtt-tui eu=host-a.example us=host-b.example --pacing burst
irtt-tui host.example --duration 30s
```
## SEE ALSO
`irtt-client(1)` for target syntax, pacing, negotiation flags, and
finite/continuous memory behavior; `irtt-server(1)`; `irtt-rs(1)`.