feat(server): add negotiation policy controls #87

Merged
Lochnair merged 3 commits from feat/server-negotiation-policy into main 2026-08-14 08:06:46 +00:00
Lochnair commented 2026-08-14 07:52:51 +00:00 (Migrated from github.com)

Summary

Adds the two remaining open-time server capability restrictions as configurable
negotiation policy:

  • a configurable timestamp allowance (dual, single, none);
  • a configurable DSCP permission (allowed or disallowed);
  • both exposed through irtt_server::ServerConfig and through the real
    irtt-server / irtt-rs server applet;
  • defaults preserve current behavior exactly — the controls are opt-in, and
    an existing configuration negotiates what it always did.

Timestamp policy

TimestampAllowance is server policy, deliberately a separate public type from
the wire StampAt: one says what a session requested, the other what this
server is willing to provide. It defaults to Dual.

  • Dual preserves the requested StampAt.
  • Single maps Both to Midpoint and preserves Send, Receive and
    Midpoint. The midpoint substitution is the observed one, not an arbitrary
    choice between the two instants.
  • None negotiates every timestamp request to StampAt::None.
  • Clock is never rewritten. The allowance restricts timestamp placement;
    the echo layout simply carries no timestamp field once the placement is none.
  • Restriction runs before the effective-session executability check. That
    ordering is load-bearing: it makes a timestamp-disabled session whose request
    omitted Clock acceptable, where the same request is silently refused under
    the default policy.

DSCP policy

  • Allowed by default, negotiating the requested raw value unchanged — including
    values outside 0..=255, which stay in Params and are transported unmarked
    as before.
  • --no-dscp / dscp_allowed=false negotiates raw Params.dscp to zero,
    for every requested value, before the session is created. The open is never
    refused over DSCP, and nothing is clamped or wrapped.
  • Params::encode omits a zero integer, so the reply carries no DSCP tag and
    decodes back to zero.
  • The runtime then naturally sends the session unmarked: the negotiated params
    are the single source of truth, so no extra Session policy bit and no
    runtime knowledge of ServerConfig were added.
  • Socket-level traffic-class support and error behavior are unchanged.

Client behavior

  • Strict negotiation rejects the changed timestamp placement and the changed
    DSCP value.
  • Loose negotiation accepts them and reports the existing
    StampAtChanged / DscpChanged restrictions, with DscpChanged keeping the
    established codepoint convention (requested 46, negotiated 0).
  • No irtt-client production changes were required, and no new client
    restriction variant was added.

Scope exclusions

  • No runtime or socket changes (no wildcard/pktinfo, recvmsg/sendmsg,
    traffic-class application, maintenance or Server::run churn).
  • No ServerFill changes.
  • No multi-listener orchestration — that remains the next architecture slice.
  • No irtt-proto changes.
  • No upstream rerun and no protocol-evidence edits.

Verification

All run and passing:

cargo fmt --all -- --check
cargo check --workspace --all-targets --all-features --locked
cargo test --workspace --all-targets --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
cargo check -p irtt-cli --no-default-features --all-targets --locked
cargo check -p irtt-cli --no-default-features --features server --all-targets --locked
cargo test -p irtt-cli --no-default-features --features server --locked

Manual CLI smoke, in-repo binaries only:

  • irtt-server --bind 127.0.0.1:<port> --timestamp-allowance single --no-dscp
    against the in-repo client requesting --tstamp both --clock both --dscp 46 --loose: the test completed successfully, and the replies carried
    server_midpoint_wall_ns / server_midpoint_mono_ns with no receive or send
    timestamp fields and no marking — StampAt -> Midpoint, DSCP -> 0.
  • The same server with a strict client: the open is refused and no target
    completes, as expected.
  • A default server with the ordinary client: no restriction, ordinary session
    succeeds.
## Summary Adds the two remaining open-time server capability restrictions as configurable negotiation policy: - a configurable **timestamp allowance** (`dual`, `single`, `none`); - a configurable **DSCP permission** (allowed or disallowed); - both exposed through `irtt_server::ServerConfig` and through the real `irtt-server` / `irtt-rs server` applet; - **defaults preserve current behavior** exactly — the controls are opt-in, and an existing configuration negotiates what it always did. ## Timestamp policy `TimestampAllowance` is server policy, deliberately a separate public type from the wire `StampAt`: one says what a session requested, the other what this server is willing to provide. It defaults to `Dual`. - `Dual` preserves the requested `StampAt`. - `Single` maps `Both` to `Midpoint` and preserves `Send`, `Receive` and `Midpoint`. The midpoint substitution is the observed one, not an arbitrary choice between the two instants. - `None` negotiates every timestamp request to `StampAt::None`. - **`Clock` is never rewritten.** The allowance restricts timestamp placement; the echo layout simply carries no timestamp field once the placement is none. - Restriction runs **before** the effective-session executability check. That ordering is load-bearing: it makes a timestamp-disabled session whose request omitted `Clock` acceptable, where the same request is silently refused under the default policy. ## DSCP policy - Allowed by default, negotiating the requested raw value unchanged — including values outside `0..=255`, which stay in `Params` and are transported unmarked as before. - `--no-dscp` / `dscp_allowed=false` negotiates raw `Params.dscp` to **zero**, for every requested value, before the session is created. The open is never refused over DSCP, and nothing is clamped or wrapped. - `Params::encode` omits a zero integer, so the reply carries no DSCP tag and decodes back to zero. - The runtime then naturally sends the session unmarked: the negotiated params are the single source of truth, so no extra `Session` policy bit and no runtime knowledge of `ServerConfig` were added. - Socket-level traffic-class support and error behavior are unchanged. ## Client behavior - Strict negotiation rejects the changed timestamp placement and the changed DSCP value. - Loose negotiation accepts them and reports the existing `StampAtChanged` / `DscpChanged` restrictions, with `DscpChanged` keeping the established codepoint convention (requested 46, negotiated 0). - **No `irtt-client` production changes were required**, and no new client restriction variant was added. ## Scope exclusions - No runtime or socket changes (no wildcard/pktinfo, `recvmsg`/`sendmsg`, traffic-class application, maintenance or `Server::run` churn). - No `ServerFill` changes. - No multi-listener orchestration — that remains the next architecture slice. - No `irtt-proto` changes. - No upstream rerun and no protocol-evidence edits. ## Verification All run and passing: ``` cargo fmt --all -- --check cargo check --workspace --all-targets --all-features --locked cargo test --workspace --all-targets --all-features --locked cargo clippy --workspace --all-targets --all-features --locked -- -D warnings RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps cargo check -p irtt-cli --no-default-features --all-targets --locked cargo check -p irtt-cli --no-default-features --features server --all-targets --locked cargo test -p irtt-cli --no-default-features --features server --locked ``` Manual CLI smoke, in-repo binaries only: - `irtt-server --bind 127.0.0.1:<port> --timestamp-allowance single --no-dscp` against the in-repo client requesting `--tstamp both --clock both --dscp 46 --loose`: the test completed successfully, and the replies carried `server_midpoint_wall_ns` / `server_midpoint_mono_ns` with no receive or send timestamp fields and no marking — `StampAt -> Midpoint`, `DSCP -> 0`. - The same server with a strict client: the open is refused and no target completes, as expected. - A default server with the ordinary client: no restriction, ordinary session succeeds.
chatgpt-codex-connector[bot] (Migrated from github.com) reviewed 2026-08-14 07:56:39 +00:00
chatgpt-codex-connector[bot] (Migrated from github.com) left a comment

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5be224d1b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

### 💡 Codex Review Here are some automated review suggestions for this pull request. **Reviewed commit:** `5be224d1b2` <details> <summary>ℹ️ About Codex in GitHub</summary> <br/> [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". </details>
@@ -53,12 +53,41 @@ pub const DEFAULT_BURST_ALLOWANCE: u32 = 5;
/// emits no final reply. See [`ServerConfig::with_idle_timeout`].
chatgpt-codex-connector[bot] (Migrated from github.com) commented 2026-08-14 07:56:39 +00:00

P2 Badge Describe Single as limiting placement, not field count

When a client negotiates Clock::Both, Single maps StampAt::Both to Midpoint but deliberately leaves the clock unchanged, so timestamp_fields emits both midpoint_wall and midpoint_mono. The public API's “at most one timestamp field” guarantee is therefore false and can mislead callers estimating reply layout or bandwidth; describe this as one timestamp instant/placement with one field per negotiated clock, and align the CLI/README wording.

AGENTS.md reference: crates/irtt-server/AGENTS.md:L273-L276

Useful? React with 👍 / 👎.

**<sub><sub>![P2 Badge](https://img.shields.io/badge/P2-yellow?style=flat)</sub></sub> Describe `Single` as limiting placement, not field count** When a client negotiates `Clock::Both`, `Single` maps `StampAt::Both` to `Midpoint` but deliberately leaves the clock unchanged, so `timestamp_fields` emits both `midpoint_wall` and `midpoint_mono`. The public API's “at most one timestamp field” guarantee is therefore false and can mislead callers estimating reply layout or bandwidth; describe this as one timestamp instant/placement with one field per negotiated clock, and align the CLI/README wording. AGENTS.md reference: [crates/irtt-server/AGENTS.md:L273-L276](https://github.com/Lochnair/irtt-rs/blob/5be224d1b2e0cc4ad2a9f2d52f7a36ed25f6a0f9/crates/irtt-server/AGENTS.md#L273-L276) Useful? React with 👍 / 👎.
Lochnair (Migrated from github.com) reviewed 2026-08-14 08:01:45 +00:00
@@ -53,12 +53,41 @@ pub const DEFAULT_BURST_ALLOWANCE: u32 = 5;
/// emits no final reply. See [`ServerConfig::with_idle_timeout`].
Lochnair (Migrated from github.com) commented 2026-08-14 08:01:45 +00:00

Reworded: Single now bounds one timestamp instant, not one field, and every place that stated otherwise says the clock is untouched so a midpoint on Clock::Both still reports one field per domain — TimestampAllowance::Single and with_timestamp_allowance rustdoc, the crate-root docs, restrict_stamp_at, the --timestamp-allowance help and value doc, README, and the server AGENTS.md bullet (which now explicitly says not to document it as one field).

🤖 Addressed by Claude Code

Reworded: `Single` now bounds one timestamp *instant*, not one field, and every place that stated otherwise says the clock is untouched so a midpoint on `Clock::Both` still reports one field per domain — `TimestampAllowance::Single` and `with_timestamp_allowance` rustdoc, the crate-root docs, `restrict_stamp_at`, the `--timestamp-allowance` help and value doc, README, and the server AGENTS.md bullet (which now explicitly says not to document it as one field). _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Sign in to join this conversation.