feat(cli): expose server negotiation policy
This commit is contained in:
4 files changed
+119
-3
No files matched your search
@@ -57,7 +57,7 @@ Tokio must remain optional for `irtt-client`; default builds must remain runtime
|
||||
|
||||
### Server architecture (`irtt-server`)
|
||||
|
||||
`irtt-server` is a first-class reusable server crate. It contains the deterministic OPEN/ECHO/session core — packet admission, authentication policy, open negotiation, the bounded session table, open reply construction, echo processing with its per-session receive state and timestamps, per-session rate limiting, session lifetime with idle expiry, the maximum-duration server-initiated close, and client-initiated close — plus a reusable Tokio UDP `Server` that runs one sequential `ServerCore` per listener with caller-controlled shutdown and scheduled idle maintenance. Logical expiry remains exact on authenticated, structurally valid requests, while the runtime also reclaims expired sessions once per second when no traffic arrives. Each reply carries the raw traffic class it must be sent with, which the runtime applies to the listener before every send. A wildcard listener additionally recovers each request's local destination from packet metadata and sends that request's reply from it, on Linux, macOS and FreeBSD; a wildcard bind on a target without that path is refused at construction rather than served from a routing-table source address. Explicit-address listeners are unaffected everywhere. Echo replies fill their payload from the session's negotiated server fill; the policy and its deliberate divergences are in `crates/irtt-server/AGENTS.md`. Multiple listeners in one process, and optional timestamp and DSCP restriction controls, are not implemented.
|
||||
`irtt-server` is a first-class reusable server crate. It contains the deterministic OPEN/ECHO/session core — packet admission, authentication policy, open negotiation, the bounded session table, open reply construction, echo processing with its per-session receive state and timestamps, per-session rate limiting, session lifetime with idle expiry, the maximum-duration server-initiated close, and client-initiated close — plus a reusable Tokio UDP `Server` that runs one sequential `ServerCore` per listener with caller-controlled shutdown and scheduled idle maintenance. Logical expiry remains exact on authenticated, structurally valid requests, while the runtime also reclaims expired sessions once per second when no traffic arrives. Each reply carries the raw traffic class it must be sent with, which the runtime applies to the listener before every send. A wildcard listener additionally recovers each request's local destination from packet metadata and sends that request's reply from it, on Linux, macOS and FreeBSD; a wildcard bind on a target without that path is refused at construction rather than served from a routing-table source address. Explicit-address listeners are unaffected everywhere. Echo replies fill their payload from the session's negotiated server fill; the policy and its deliberate divergences are in `crates/irtt-server/AGENTS.md`. Two optional negotiation policies restrict what a session may ask the server to provide — a timestamp allowance and DSCP permission — both off by default and both settled during open negotiation rather than in the runtime. Serving multiple listeners in one process is not implemented; that, and ordinary process-level polish and observability decisions, is what remains of the server architecture.
|
||||
|
||||
- It shares wire semantics with the client through `irtt-proto` and follows the same clean-room boundary.
|
||||
- It must **not** depend on `irtt-client` merely to reuse protocol behavior.
|
||||
|
||||
@@ -202,6 +202,23 @@ irtt-server \
|
||||
--idle-timeout 30s
|
||||
```
|
||||
|
||||
Two optional controls restrict what the server will provide a session, and both
|
||||
are off unless asked for:
|
||||
|
||||
```sh
|
||||
irtt-server \
|
||||
--bind 192.0.2.10:2112 \
|
||||
--timestamp-allowance single \
|
||||
--no-dscp
|
||||
```
|
||||
|
||||
`--timestamp-allowance` takes `dual` (the default, honoring every requested
|
||||
placement), `single`, which provides at most one timestamp and answers a request
|
||||
for both receive and send timestamps with the midpoint, or `none`, which provides
|
||||
no timestamps. The requested clock source is never changed. `--no-dscp`
|
||||
negotiates any requested DSCP to zero, so clients are told their echo replies
|
||||
will be unmarked, and they are sent unmarked.
|
||||
|
||||
To require authentication, pass the shared key both sides use:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
|
||||
use clap::Parser;
|
||||
use irtt_server::ServerConfig;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use irtt_server::{ServerConfig, TimestampAllowance};
|
||||
|
||||
// Every policy option is optional. An omitted option leaves the corresponding
|
||||
// `ServerConfig` default in place rather than restating it here, so library and
|
||||
@@ -79,6 +79,46 @@ pub struct ServerArgs {
|
||||
long_help = "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."
|
||||
)]
|
||||
pub max_duration: Option<Duration>,
|
||||
|
||||
/// How many timestamps this server will provide.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "MODE",
|
||||
value_enum,
|
||||
long_help = "How many timestamps this server will provide.\n\ndual: honor a request for send, receive, both or midpoint timestamps.\nsingle: provide at most one timestamp; a request for both is negotiated to midpoint.\nnone: provide no timestamps at all.\n\nThe requested clock source is never changed, only the timestamp placement. Omit this flag to honor every requested placement."
|
||||
)]
|
||||
pub timestamp_allowance: Option<TimestampAllowanceArg>,
|
||||
|
||||
/// Refuse to provide requested DSCP marking.
|
||||
#[arg(
|
||||
long,
|
||||
long_help = "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."
|
||||
)]
|
||||
pub no_dscp: bool,
|
||||
}
|
||||
|
||||
/// The command-line spelling of [`TimestampAllowance`].
|
||||
///
|
||||
/// A CLI-local enum rather than a `clap` derive on the library type: `irtt-server`
|
||||
/// is a reusable crate and has no business depending on an argument parser.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
|
||||
pub enum TimestampAllowanceArg {
|
||||
/// Honor every requested timestamp placement.
|
||||
Dual,
|
||||
/// Provide at most one timestamp; both becomes midpoint.
|
||||
Single,
|
||||
/// Provide no timestamps.
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<TimestampAllowanceArg> for TimestampAllowance {
|
||||
fn from(value: TimestampAllowanceArg) -> Self {
|
||||
match value {
|
||||
TimestampAllowanceArg::Dual => Self::Dual,
|
||||
TimestampAllowanceArg::Single => Self::Single,
|
||||
TimestampAllowanceArg::None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerArgs {
|
||||
@@ -107,6 +147,14 @@ impl ServerArgs {
|
||||
if let Some(value) = self.max_duration {
|
||||
config = config.with_max_test_duration(value);
|
||||
}
|
||||
if let Some(value) = self.timestamp_allowance {
|
||||
config = config.with_timestamp_allowance(value.into());
|
||||
}
|
||||
// A negative capability flag, so absence is the ordinary enabled state
|
||||
// and the library default is what an unset flag leaves in place.
|
||||
if self.no_dscp {
|
||||
config = config.with_dscp_allowed(false);
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
@@ -224,6 +272,55 @@ mod tests {
|
||||
assert_eq!(config.max_test_duration(), Some(Duration::from_secs(120)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capability_restrictions_map_onto_the_server_config() {
|
||||
let config = bound(&["--timestamp-allowance", "single", "--no-dscp"]).server_config();
|
||||
|
||||
assert_eq!(
|
||||
config.timestamp_allowance(),
|
||||
TimestampAllowance::Single,
|
||||
"a request for both timestamps is negotiated to midpoint"
|
||||
);
|
||||
assert!(!config.dscp_allowed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_timestamp_allowance_mode_parses() {
|
||||
for (argument, expected) in [
|
||||
("dual", TimestampAllowance::Dual),
|
||||
("single", TimestampAllowance::Single),
|
||||
("none", TimestampAllowance::None),
|
||||
] {
|
||||
assert_eq!(
|
||||
bound(&["--timestamp-allowance", argument])
|
||||
.server_config()
|
||||
.timestamp_allowance(),
|
||||
expected,
|
||||
"--timestamp-allowance {argument}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(parse(&[
|
||||
"--bind",
|
||||
"127.0.0.1:2112",
|
||||
"--timestamp-allowance",
|
||||
"midpoint"
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse(&["--bind", "127.0.0.1:2112", "--timestamp-allowance", "both"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_capability_restrictions_are_off_when_their_flags_are_absent() {
|
||||
// Explicitly, and not only through the whole-config comparison above:
|
||||
// these are the two values that would silently change every existing
|
||||
// deployment's negotiation if the CLI restated them.
|
||||
let config = bound(&["--idle-timeout", "30s"]).server_config();
|
||||
|
||||
assert_eq!(config.timestamp_allowance(), TimestampAllowance::Dual);
|
||||
assert!(config.dscp_allowed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_zero_policy_values_are_preserved() {
|
||||
let config = bound(&[
|
||||
|
||||
@@ -80,6 +80,8 @@ fn canonical_server_subcommand_dispatches_to_server() {
|
||||
assert!(text.contains("--bind <ADDR>"));
|
||||
assert!(text.contains("--max-sessions <COUNT>"));
|
||||
assert!(text.contains("--idle-timeout <DURATION>"));
|
||||
assert!(text.contains("--timestamp-allowance <MODE>"));
|
||||
assert!(text.contains("--no-dscp"));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "server"))]
|
||||
|
||||
Reference in new issue
Block a user