Commit Graph
724 Commits
Author SHA1 Message Date
Nils Andreas Svee e0dec9ac5f refactor(stats): make late-reply sampling explicit
The TUI suppressed late-reply timing by rebuilding the event with sent_at,
rtt, server_timing, one_way and received_stats erased. That leaked stats
policy into event construction and, as a side effect, reclassified a matched
late reply as an untracked late reply.

StatsConfig now carries an explicit LateReplyMode. Measure keeps today's
behavior for every existing caller. CountOnly keeps the reply a late unique
reply and keeps packet, byte and late counters updating, while suppressing
timing, one-way, server-processing and IPDV contributions and reporting
contributed_sample == false. Cumulative and rolling snapshots share the
policy. Genuinely untracked late replies stay untracked in both modes.

The TUI selects CountOnly and passes the original event straight to the
collector.
2026-08-15 01:17:46 +07:00
Nils Andreas Svee 69b90dd64a refactor(cli): unify scalar presentation formatting
Stream output, the run summary, and the TUI each carried their own copy of
the same scalar vocabulary, and they had drifted: the TUI spelled
microseconds "us" while the others used "µs", percentages guarded
non-finite values in one place but not another, and the absent-value
placeholder was spelled inline everywhere.

cmd::format now owns nanosecond-scalar, percentage, and count formatting
with one documented unit-selection and rounding policy, µs everywhere, and
signed values preserved. The frontends keep their own metric sets and
layouts; the TUI keeps its coarse wall-clock span policy, now named
format_span and documented as deliberately distinct, and uses the shared
scalar vocabulary below one second.
2026-08-15 01:11:33 +07:00
Nils Andreas Svee e3003c348f refactor(client): make managed failure classification exhaustive
classify_client_error now matches every ClientError variant explicitly, so a
new variant fails to compile here until its real classification is chosen.
NoTokioRuntime classifies as InvalidState and DatagramLengthMismatch as
Socket; ManagedTargetFailureKind::Other survives only as a deliberate
category with no default path into it.

ManagedTargetFailurePhase and ManagedTargetFailureKind gain controlled
Display impls, and the CLI client/TUI failure diagnostics use them instead
of leaking Debug enum spelling into user-facing text.
2026-08-15 01:03:35 +07:00
Nils Andreas Svee 90c04b6569 refactor(cli): simplify feature flags and make stats mandatory for clients
Remove the optional "stats" feature by merging it into the "client" and "tui" features to reduce conditional compilation complexity. Replace the "client-runtime" internal feature with direct dependency sets.
2026-08-15 00:24:28 +07:00
Nils Andreas Svee 88419d4144 build(cli): enable ancillary feature for client-runtime
Add the ancillary feature dependency to client-runtime to support advanced socket operations and metadata handling.
2026-08-14 22:23:33 +07:00
Nils Andreas Svee 25616969c4 Merge pull request #89 from Lochnair/test/real-server-peers
test: prefer real server for compliant peers
2026-08-14 18:25:24 +07:00
Nils Andreas Svee 446028671b test(cli): use real server for compliant peers 2026-08-14 17:21:22 +07:00
Nils Andreas Svee fca7a2fcc6 test(client): use real server for compliant peers 2026-08-14 17:17:41 +07:00
Nils Andreas Svee cbef62be7a Merge pull request #88 from Lochnair/feat/server-multi-listener
feat(server): add multi-listener server set
2026-08-14 16:46:45 +07:00
Nils Andreas Svee 2171d02203 feat(cli): route server through server set
`--bind` becomes repeatable, so one process can serve several addresses:
an explicit IPv4 and IPv6 pair on one port, or two interface addresses
that must stay distinct.

The applet now builds a `ServerSet` for every invocation, including a
single bind. There is deliberately no bind-count branch and no direct
path from the CLI to `Server`: one orchestration path means startup
errors, shutdown and listener-failure handling are identical whatever was
configured, and the single-listener case — the common one — keeps that
path exercised in normal use rather than only in tests.

The configuration stays one `ServerConfig`; addresses never enter it, and
`ServerSet` clones it into each listener. Bounds in it are therefore per
listener, which `--max-sessions` help now says. Listener lines are
printed only once the whole set is up, so a set whose second bind fails
no longer announces the first as listening.

The shutdown flag, its polling future and the current-thread runtime are
unchanged: exactly one shutdown future is created and the set fans it out
internally.
2026-08-14 16:34:02 +07:00
Nils Andreas Svee 09f0d6e02e feat(server): add multi-listener server set
`Server` answers on one address. A host that must serve several — an
explicit IPv4 and IPv6 pair, or two interface addresses — needs several
of them, and something has to own their construction, their shutdown and
each other's failures.

`ServerSet` is that owner, and deliberately not a protocol object: it
holds no session state, no tokens and no clock, and nothing it does is
visible on the wire. Each listener stays a complete `Server` with its own
socket, core, session table and tokens, so a token issued by one is
unknown at the others and every bound in the cloned `ServerConfig` —
`max_sessions` included — applies per listener rather than per process.

Construction binds every listener or none, preserving requested order and
recording the addresses actually bound. `run` moves each listener into
its own task, fans one shutdown future out over a watch channel, and
joins them all: a listener that fails, or that returns before shutdown
was asked for, shuts its siblings down and fails the set, because a
service configured for two families must not continue silently as one.

A genuine IPv6 listener sharing an explicit port with an IPv4 sibling is
created `IPV6_V6ONLY` before it is bound, since a dual-stack socket would
otherwise claim the IPv4 port too. That is the only pre-bind step, it
uses safe `socket2` APIs, and an IPv4-mapped address is read as the IPv4
listener it is — never as the IPv6 half of a pair. Standalone
`Server::bind` semantics are unchanged.
2026-08-14 16:04:46 +07:00
Nils Andreas Svee fabd698187 Merge pull request #87 from Lochnair/feat/server-negotiation-policy
feat(server): add negotiation policy controls
2026-08-14 15:06:45 +07:00
Nils Andreas Svee 8607720219 docs(server): describe single allowance as one instant 2026-08-14 15:00:38 +07:00
Nils Andreas Svee 5be224d1b2 feat(cli): expose server negotiation policy 2026-08-14 14:30:28 +07:00
Nils Andreas Svee 8308c077eb feat(server): add negotiation policy controls 2026-08-14 14:24:57 +07:00
Nils Andreas Svee 58c04e8d04 Merge pull request #86 from Lochnair/feat/server-fill-policy
feat(server): implement server fill
2026-08-14 13:54:16 +07:00
Nils Andreas Svee a471fb3bfd docs: distinguish no fill preference from none 2026-08-14 13:07:39 +07:00
Nils Andreas Svee e4239f45c5 feat(server): generate echo payload fill 2026-08-14 12:58:30 +07:00
Nils Andreas Svee 36500e1ea0 feat(server): negotiate server fill 2026-08-14 12:32:37 +07:00
Nils Andreas Svee 24465bc1da Merge pull request #85 from Lochnair/feat/server-wildcard-source-selection
feat(server): pin wildcard reply source address
2026-08-14 12:11:17 +07:00
Nils Andreas Svee aed7eef454 fix(server): mark a mapped listener with the IPv4 option 2026-08-14 11:49:05 +07:00
Nils Andreas Svee 2d88939ed4 fix(server): pin the reply source, not the route 2026-08-14 11:31:10 +07:00
Nils Andreas Svee e154508cb1 fix(server): treat a mapped wildcard bind as wildcard 2026-08-14 00:50:25 +07:00
Nils Andreas Svee 3c554a78d7 test(server): verify wildcard source selection 2026-08-14 00:30:02 +07:00
Nils Andreas Svee 265b857b84 feat(server): pin wildcard reply source 2026-08-14 00:20:17 +07:00
Nils Andreas Svee 047a842f2d Merge pull request #84 from Lochnair/feat/server-reply-traffic-class
feat(server): apply reply traffic class
2026-08-13 23:12:59 +07:00
Nils Andreas Svee 965369ae2a docs(server): drop a private link from public docs 2026-08-13 22:30:54 +07:00
Nils Andreas Svee 1830429ce5 fix(server): keep unmarkable listeners serving 2026-08-13 22:26:58 +07:00
Nils Andreas Svee 5420387846 feat(server): apply reply traffic class 2026-08-13 20:40:07 +07:00
Nils Andreas Svee a4a5219b9e feat(server): carry reply traffic class 2026-08-13 20:34:42 +07:00
Nils Andreas Svee 68e7d18c12 Merge pull request #83 from Lochnair/feat/cli-server-applet
feat(cli): add server applet
2026-08-13 19:49:54 +07:00
Nils Andreas Svee 565d7fb163 fix(cli): ignore the executable suffix in argv0 dispatch 2026-08-13 19:17:20 +07:00
Nils Andreas Svee a672f78e86 feat(cli): add server applet 2026-08-13 19:01:39 +07:00
Nils Andreas Svee ae23e410bb Merge pull request #82 from Lochnair/test/upstream-client-interop
test(server): validate upstream 0.9.1 client interop
2026-08-13 18:19:43 +07:00
Nils Andreas Svee 9d7566955d docs(server): drop the unobserved continuous-mode wire claim
The continuous-request row said a -d 0 request never reaches the wire,
which no observation in this validation supports: the run took no packet
capture and the server was not instrumented for datagram arrival, so
what the client emitted before exiting was never seen.

Record what was actually observed — the argument was refused, the client
exited non-zero, and it printed none of the connection line it prints in
every other scenario — and state plainly that the emission was not
observed. The conclusion that matters is unchanged: continuous mode was
not exercisable through this CLI.

No implementation, test or protocol evidence changes.
2026-08-13 17:39:28 +07:00
Nils Andreas Svee 750e28200a docs(server): scope the strict-mode restriction claim
The environment notes said restriction scenarios were run both with and
without --loose, but the matrix records a strict run for the
packet-length restriction only; the interval and maximum-duration rows
were --loose alone. Say that, so the artifact does not credit
strict-mode coverage it never recorded.

No implementation, test or protocol evidence changes.
2026-08-13 17:35:21 +07:00
Nils Andreas Svee 02bdb495a1 docs(server): qualify interop validation claims
Two claims in the validation record were stronger than the evidence
behind them.

The maximum-duration Close conclusion was stated for every conforming
client. That does not follow: the deadline is evaluated on arrival, not
on transmission, so an echo sent legitimately before the negotiated
Duration elapsed can still be queued long enough to arrive after the
server's two-second grace and carry Close. Scope the result to the
loopback run that was actually performed, and describe the Close as a
termination path for traffic continuing or arriving beyond the grace,
including delayed in-flight traffic.

The document also claimed the validation was reproducible from the
commands above, while carrying only summarized client arguments and no
harness — the harness was disposable on purpose and is not being
promoted into repository infrastructure. Say plainly that this is a
record of the validation performed rather than a reproduction harness.

No implementation, test or protocol evidence changes.
2026-08-13 17:15:37 +07:00
Nils Andreas Svee a50edff624 docs(server): record upstream client interop
Run the released upstream IRTT 0.9.1 client as a black box against the
Tokio server over explicit loopback listeners, and record the result.

Twelve of thirteen matrix rows pass and one is skipped for a client-side
CLI reason: IPv4 and IPv6 ordinary sessions, HMAC with a wrong-key
negative control, packet-length and interval restrictions both strictly
and with --loose, maximum-duration restriction, no-test, and midpoint
timestamps on each clock selection. No incompatibility was found, so
nothing in the implementation changed.

Two results are worth keeping. The single-field midpoint reply this
server deliberately emits instead of upstream 0.9.1's dual-field form is
accepted by the released client, which derives RTT and IPDV from it. And
the maximum-duration close is unreachable by a conforming client: the
deadline is the configured maximum plus two seconds while negotiation
caps the session Duration at that same maximum, so a client honoring the
Duration it was given always stops early. The two-second grace is the
measured margin already recorded in the protocol evidence, so this is
the implementation matching that evidence rather than a defect, and the
close path stays covered by the lifecycle tests.

Upstream source, tests and history were not inspected; only the
executable's version, help, exit status and normal output were observed.
No test in this repository depends on the upstream binary.
2026-08-13 16:47:09 +07:00
Nils Andreas Svee ace3defe22 Merge pull request #81 from Lochnair/feat/server-tokio-runtime
feat(server): add Tokio UDP runtime
2026-08-13 11:35:06 +07:00
Nils Andreas Svee 8d240e352d docs(server): update runtime status 2026-08-13 02:06:41 +07:00
Nils Andreas Svee 41e39bd5b9 fix(server): keep shutdown responsive during sends 2026-08-13 01:50:25 +07:00
Nils Andreas Svee 85c432fcf8 test(server): exercise async client interop 2026-08-13 01:35:34 +07:00
Nils Andreas Svee 4fb5ca6d15 feat(server): add tokio udp runtime 2026-08-13 01:32:05 +07:00
Nils Andreas Svee d45f607b9c Merge pull request #80 from Lochnair/feat/server-lifecycle-policy
feat(server): enforce rate and session lifetime
2026-08-13 01:06:55 +07:00
Nils Andreas Svee c7537b9c42 fix(server): take the idle interval cap before saturating 2026-08-13 00:55:23 +07:00
Nils Andreas Svee c2994f64a4 feat(server): enforce rate and session lifetime 2026-08-13 00:45:40 +07:00
Nils Andreas Svee 683b52d334 feat(server): add lifetime negotiation policy 2026-08-13 00:24:42 +07:00
Nils Andreas Svee 391748f907 Merge pull request #79 from Lochnair/feat/server-echo-core
feat(server): implement echo core
2026-08-12 23:52:44 +07:00
Nils Andreas Svee cce2ff731a fix(server): order an echo reply's timestamp pair 2026-08-12 23:47:34 +07:00
Nils Andreas Svee 8abab7ee38 feat(server): implement echo core 2026-08-12 23:21:02 +07:00