Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb8bea4e6 | ||
|
|
b3241958d9 | ||
|
|
8644b70b89 | ||
|
|
196512068b | ||
|
|
e861de5247 | ||
|
|
d335ad78c9 | ||
|
|
f650b6220a | ||
|
|
865c166828 | ||
|
|
bc2bf34ed4 | ||
|
|
4864357486 | ||
|
|
188d6e5354 | ||
|
|
2882beb2c9 | ||
|
|
ed805e854f | ||
|
|
f2e9c72b22 | ||
|
|
4563816873 | ||
|
|
70a2998f8c | ||
|
|
4ddefb2101 | ||
|
|
04291a0b7b | ||
|
|
c426884fd1 |
No files matched your search
@@ -158,3 +158,6 @@ jobs:
|
|||||||
--all-features \
|
--all-features \
|
||||||
--no-deps \
|
--no-deps \
|
||||||
--locked
|
--locked
|
||||||
|
|
||||||
|
- name: Run doctests
|
||||||
|
run: cargo test --workspace --doc --all-features --locked
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
# (native builds, no emulation), but buildx still needs a driver that
|
# (native builds, no emulation), but buildx still needs a driver that
|
||||||
# can assemble a multi-platform manifest list.
|
# can assemble a multi-platform manifest list.
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
|
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4
|
||||||
|
|
||||||
- name: Log in to Gitea Container Registry
|
- name: Log in to Gitea Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
# below), never a separate build, so the hash matches everywhere.
|
# below), never a separate build, so the hash matches everywhere.
|
||||||
- name: Build and push to Gitea
|
- name: Build and push to Gitea
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Proto Fuzz Smoke
|
name: Fuzz Smoke
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
# regression; this is not meant to be exhaustive fuzzing.
|
# regression; this is not meant to be exhaustive fuzzing.
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
for target in decode_request decode_open_reply decode_echo_reply decode_params decode_varint; do
|
for target in decode_request decode_open_reply decode_echo_reply decode_params decode_varint server_core_datagrams; do
|
||||||
echo "::group::fuzz smoke: ${target}"
|
echo "::group::fuzz smoke: ${target}"
|
||||||
RUSTUP_TOOLCHAIN="${FUZZ_TOOLCHAIN}" cargo fuzz run --target "${FUZZ_TARGET}" "${target}" -- \
|
RUSTUP_TOOLCHAIN="${FUZZ_TOOLCHAIN}" cargo fuzz run --target "${FUZZ_TARGET}" "${target}" -- \
|
||||||
-max_total_time=10 \
|
-max_total_time=10 \
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
# It remains here while portable actionlint runs with Gitea CI, but scans
|
# It remains here while portable actionlint runs with Gitea CI, but scans
|
||||||
# both workflow directories through its GitHub Actions rule set.
|
# both workflow directories through its GitHub Actions rule set.
|
||||||
- name: Run zizmor
|
- name: Run zizmor
|
||||||
uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3
|
uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4
|
||||||
with:
|
with:
|
||||||
persona: regular
|
persona: regular
|
||||||
inputs: |
|
inputs: |
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ permissions:
|
|||||||
jobs:
|
jobs:
|
||||||
scan-scheduled:
|
scan-scheduled:
|
||||||
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }}
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
||||||
with:
|
with:
|
||||||
# Example of specifying custom arguments
|
# Example of specifying custom arguments
|
||||||
scan-args: |-
|
scan-args: |-
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
./
|
./
|
||||||
scan-pr:
|
scan-pr:
|
||||||
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
if: ${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}
|
||||||
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67" # v2.5.1
|
uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@a345acffa64b0eaede81a3d9aae6141214d9c8fc" # v2.6.0
|
||||||
with:
|
with:
|
||||||
# Example of specifying custom arguments
|
# Example of specifying custom arguments
|
||||||
scan-args: |-
|
scan-args: |-
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
- name: Upload Clippy SARIF
|
- name: Upload Clippy SARIF
|
||||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
sarif_file: rust-clippy-results.sarif
|
sarif_file: rust-clippy-results.sarif
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ Use the narrowest useful checks first. For substantive workspace changes, final
|
|||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
cargo check --workspace --all-targets --all-features --locked
|
cargo check --workspace --all-targets --all-features --locked
|
||||||
cargo test --workspace --all-targets --all-features --locked
|
cargo test --workspace --all-targets --all-features --locked
|
||||||
|
cargo test --workspace --doc --all-features --locked
|
||||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ when to use it instead of `irtt-client`/`irtt-server` directly.
|
|||||||
```sh
|
```sh
|
||||||
cargo fmt --all -- --check
|
cargo fmt --all -- --check
|
||||||
cargo test --workspace --locked
|
cargo test --workspace --locked
|
||||||
|
cargo test --workspace --doc --all-features --locked
|
||||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ pub fn detect_applet_from_argv0(argv0: &str) -> Option<RequestedApplet> {
|
|||||||
match applet_basename(argv0) {
|
match applet_basename(argv0) {
|
||||||
"irtt-client" => Some(RequestedApplet::Client),
|
"irtt-client" => Some(RequestedApplet::Client),
|
||||||
"irtt-tui" => Some(RequestedApplet::Tui),
|
"irtt-tui" => Some(RequestedApplet::Tui),
|
||||||
"irttd" | "irtt-server" => Some(RequestedApplet::Server),
|
"irtt-server" => Some(RequestedApplet::Server),
|
||||||
"irtt-rs" => None,
|
"irtt-rs" => None,
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -199,6 +199,7 @@ mod tests {
|
|||||||
detect_applet_from_argv0("irtt-server"),
|
detect_applet_from_argv0("irtt-server"),
|
||||||
Some(RequestedApplet::Server)
|
Some(RequestedApplet::Server)
|
||||||
);
|
);
|
||||||
|
assert_eq!(detect_applet_from_argv0("irttd"), None);
|
||||||
assert_eq!(detect_applet_from_argv0("irtt-rs"), None);
|
assert_eq!(detect_applet_from_argv0("irtt-rs"), None);
|
||||||
assert_eq!(detect_applet_from_argv0("custom-name"), None);
|
assert_eq!(detect_applet_from_argv0("custom-name"), None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,10 +106,22 @@ pub struct ServerArgs {
|
|||||||
pub timestamp_allowance: Option<TimestampAllowanceArg>,
|
pub timestamp_allowance: Option<TimestampAllowanceArg>,
|
||||||
|
|
||||||
/// Refuse to provide requested DSCP marking.
|
/// Refuse to provide requested DSCP marking.
|
||||||
|
//
|
||||||
|
// `FalseyValueParser` rather than clap's default `bool` parser for a flag,
|
||||||
|
// which is what makes IRTT_SERVER_NO_DSCP usable from the environment at
|
||||||
|
// all. A flag's default parser accepts only the literals `true` and
|
||||||
|
// `false`, so the ordinary container/orchestrator spelling
|
||||||
|
// `IRTT_SERVER_NO_DSCP=1` did not enable the flag — it failed argument
|
||||||
|
// parsing and the server refused to start. Falsey semantics are clap's own
|
||||||
|
// documented recipe for a boolean environment flag: `0`, `false`, `no`,
|
||||||
|
// `n`, `off` and empty are off, anything else is on, and an absent
|
||||||
|
// variable is off as before. The command-line flag is unaffected: it still
|
||||||
|
// takes no value and still means "on".
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
env = "IRTT_SERVER_NO_DSCP",
|
env = "IRTT_SERVER_NO_DSCP",
|
||||||
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."
|
value_parser = clap::builder::FalseyValueParser::new(),
|
||||||
|
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.\n\nIRTT_SERVER_NO_DSCP sets the same thing from the environment: 0, false, no, n, off and an empty value leave marking enabled, and any other value disables it."
|
||||||
)]
|
)]
|
||||||
pub no_dscp: bool,
|
pub no_dscp: bool,
|
||||||
}
|
}
|
||||||
@@ -548,6 +560,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `IRTT_SERVER_NO_DSCP` is the one policy variable whose value is a
|
||||||
|
/// boolean rather than a number, a duration or a string, and the
|
||||||
|
/// container/orchestrator deployments this whole environment surface
|
||||||
|
/// exists for spell booleans `1`/`0`. clap's default parser for a flag
|
||||||
|
/// accepts only the literals `true`/`false` and turns everything else
|
||||||
|
/// into a *parse failure*, so `IRTT_SERVER_NO_DSCP=1` used to stop the
|
||||||
|
/// server from starting at all rather than enabling the flag. Falsey
|
||||||
|
/// semantics are what keeps that from being a startup failure.
|
||||||
|
#[test]
|
||||||
|
fn the_no_dscp_environment_variable_accepts_ordinary_boolean_spellings() {
|
||||||
|
for value in ["1", "true", "TRUE", "yes", "on", "enabled"] {
|
||||||
|
with_env(&[("IRTT_SERVER_NO_DSCP", value)], || {
|
||||||
|
let config = parse(&["--bind", "127.0.0.1:2112"])
|
||||||
|
.expect("a truthy value must not fail argument parsing")
|
||||||
|
.server_config();
|
||||||
|
assert!(!config.dscp_allowed(), "IRTT_SERVER_NO_DSCP={value}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for value in ["0", "false", "no", "off", ""] {
|
||||||
|
with_env(&[("IRTT_SERVER_NO_DSCP", value)], || {
|
||||||
|
let config = parse(&["--bind", "127.0.0.1:2112"])
|
||||||
|
.expect("a falsey value must not fail argument parsing")
|
||||||
|
.server_config();
|
||||||
|
assert!(config.dscp_allowed(), "IRTT_SERVER_NO_DSCP={value}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The command-line flag stays a flag: it takes no value, whatever the
|
||||||
|
/// environment parser accepts.
|
||||||
|
#[test]
|
||||||
|
fn the_no_dscp_flag_still_takes_no_command_line_value() {
|
||||||
|
assert!(parse(&["--bind", "127.0.0.1:2112", "--no-dscp=false"]).is_err());
|
||||||
|
assert!(!bound(&["--no-dscp"]).server_config().dscp_allowed());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_explicit_flag_overrides_the_environment() {
|
fn an_explicit_flag_overrides_the_environment() {
|
||||||
with_env(&[("IRTT_SERVER_MAX_SESSIONS", "512")], || {
|
with_env(&[("IRTT_SERVER_MAX_SESSIONS", "512")], || {
|
||||||
|
|||||||
@@ -26,30 +26,30 @@ configuration (`try_enable_tx_timestamping` restores it explicitly) and
|
|||||||
timing falls back to userspace `sent_at` as it always has. TX capability
|
timing falls back to userspace `sent_at` as it always has. TX capability
|
||||||
failure is never a public error and is never logged from the library.
|
failure is never a public error and is never logged from the library.
|
||||||
|
|
||||||
### Correlation invariant: kernel ID == wire sequence
|
### Best-effort kernel-ID correlation
|
||||||
|
|
||||||
The client uses the kernel's automatic `OPT_ID` directly as the probe's wire
|
The client uses the kernel's automatic `OPT_ID` as the probe's wire-sequence
|
||||||
sequence number — no separate correlation map. This is safe because:
|
correlation ID — no separate correlation map. On the normal successful-send
|
||||||
|
path, both counters start at zero after Open and advance together. This is
|
||||||
|
not a permanent identity: a kernel ID can theoretically be consumed while a
|
||||||
|
send is being prepared and then survive a subsequent send failure, leaving
|
||||||
|
kernel IDs one or more steps ahead of `next_wire_seq`. Do not restore an
|
||||||
|
assumption that failed or nonblocking sends cannot cause that desynchrony.
|
||||||
|
|
||||||
- **A failed/nonblocking send never consumes an ID.** Linux increments the
|
The effect is confined to the optional timing enhancement. A kernel timestamp
|
||||||
per-socket ID counter (`sk->sk_tskey`) speculatively while building a
|
whose ID cannot be matched to a pending or retained timed-out probe is
|
||||||
datagram and rolls the increment back (`atomic_dec`) on every error path
|
discarded, as is any implausible timestamp; `sent_at` remains the ordinary
|
||||||
out of `__ip_append_data`/`__ip6_append_data`
|
userspace fallback. **After every failed probe submission, disable kernel-ID
|
||||||
(`net/ipv4/ip_output.c`, `net/ipv6/ip6_output.c`), guarded by a local
|
correlation for the rest of that session.** A gap means an otherwise plausible
|
||||||
`hold_tskey` flag. A `WouldBlock` `try_send` therefore can never leave a
|
later ID can name the wrong probe, so subsequent error-queue timestamps must
|
||||||
gap in, or otherwise advance, the ID the kernel will assign to the next
|
be discarded rather than attached by ID. This is safe because:
|
||||||
*successful* submission. (`include/net/sock.h`'s `_sock_tx_timestamp`
|
|
||||||
performs the same increment for the non-corked path.) Client-side, the
|
- **Normal successful sends track together.** `wire_seq` starts at 0 on every
|
||||||
existing send transaction only calls `commit_probe_sent` — which is the
|
successful Open (`SessionMachine::commit_open` builds a fresh
|
||||||
only thing that advances `next_wire_seq` — after a successful socket send;
|
`ActiveSession` with `next_wire_seq: 0`) and advances by exactly 1 only via
|
||||||
a `WouldBlock` result never reaches it.
|
`commit_probe_sent`, after a confirmed successful socket send. TX
|
||||||
- **The two counters start together and advance together.** `wire_seq`
|
timestamping is enabled after that same successful Open and before any probe
|
||||||
starts at 0 on every successful Open (`SessionMachine::commit_open` builds
|
is sent, so the normal zero points line up.
|
||||||
a fresh `ActiveSession` with `next_wire_seq: 0`) and advances by exactly 1
|
|
||||||
only via `commit_probe_sent`, which only runs after a confirmed successful
|
|
||||||
send — the same condition under which the kernel's counter advances by 1.
|
|
||||||
TX timestamping is enabled only after that same successful Open and before
|
|
||||||
any probe is sent, so both counters' zero points line up exactly.
|
|
||||||
- **One socket, one session, no reopen.** `SessionMachine`'s state machine
|
- **One socket, one session, no reopen.** `SessionMachine`'s state machine
|
||||||
only reaches `Open` from `Connected`, and `commit_open`/`commit_local_close`
|
only reaches `Open` from `Connected`, and `commit_open`/`commit_local_close`
|
||||||
never route back to `Connected`. A `Client`/`AsyncClient` cannot reopen a
|
never route back to `Connected`. A `Client`/`AsyncClient` cannot reopen a
|
||||||
|
|||||||
@@ -608,8 +608,14 @@ impl AsyncClient {
|
|||||||
// No sent_at measurement, no machine commit, no schedule
|
// No sent_at measurement, no machine commit, no schedule
|
||||||
// commit: the transaction is fully discarded and retried
|
// commit: the transaction is fully discarded and retried
|
||||||
// with a fresh send_anchor on the next loop iteration.
|
// with a fresh send_anchor on the next loop iteration.
|
||||||
Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
|
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
|
||||||
Err(error) => return Poll::Ready(Err(ClientError::Socket(error))),
|
self.machine.invalidate_kernel_tx_correlation();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
self.machine.invalidate_kernel_tx_correlation();
|
||||||
|
return Poll::Ready(Err(ClientError::Socket(error)));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
|
|||||||
@@ -163,12 +163,13 @@ impl Client {
|
|||||||
///
|
///
|
||||||
/// On success, returns the negotiated open outcome and transitions the
|
/// On success, returns the negotiated open outcome and transitions the
|
||||||
/// client into either an open probe session or completed no-test state.
|
/// client into either an open probe session or completed no-test state.
|
||||||
/// Open attempts use [`ClientConfig::open_timeouts`]. Malformed, unrelated,
|
/// Open attempts use [`ClientConfig::open_timeouts`]. Malformed or
|
||||||
/// or unauthenticated datagrams are ignored until the current attempt's
|
/// unrelated datagrams are ignored until the current attempt's absolute
|
||||||
/// absolute deadline, so one attempt may consume several datagrams without
|
/// deadline, so one attempt may consume several datagrams without
|
||||||
/// retransmitting. Silence or ignored traffic eventually produces
|
/// retransmitting. Silence or ignored traffic eventually produces
|
||||||
/// [`ClientError::OpenTimeout`], while authenticated incompatibility remains
|
/// [`ClientError::OpenTimeout`]. A structurally recognizable malformed or
|
||||||
/// terminal.
|
/// incompatible Open reply from the connected peer is terminal; when HMAC
|
||||||
|
/// is configured, that recognition additionally requires authentication.
|
||||||
///
|
///
|
||||||
/// When a trusted reply allocates a token but later negotiation or socket
|
/// When a trusted reply allocates a token but later negotiation or socket
|
||||||
/// preparation fails, the client sends a best-effort cleanup close and
|
/// preparation fails, the client sends a best-effort cleanup close and
|
||||||
@@ -523,11 +524,18 @@ impl Client {
|
|||||||
.unwrap_or_else(Instant::now);
|
.unwrap_or_else(Instant::now);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if fail_send {
|
if fail_send {
|
||||||
|
runtime.invalidate_kernel_tx_correlation();
|
||||||
return Err(ClientError::Socket(io::Error::other(
|
return Err(ClientError::Socket(io::Error::other(
|
||||||
"injected probe send failure",
|
"injected probe send failure",
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let bytes = socket.send(&prepared.bytes)?;
|
let bytes = match socket.send(&prepared.bytes) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(error) => {
|
||||||
|
runtime.invalidate_kernel_tx_correlation();
|
||||||
|
return Err(ClientError::Socket(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
let send_finished_at = Instant::now();
|
let send_finished_at = Instant::now();
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1613,6 +1613,30 @@ impl ManagedClientTask {
|
|||||||
self.send_gate = self.send_gate.filter(|gate| *gate > now);
|
self.send_gate = self.send_gate.filter(|gate| *gate > now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The next stagger gate after a send was accepted at `accepted_at`.
|
||||||
|
///
|
||||||
|
/// The gate advances on its **own** cadence rather than being re-anchored
|
||||||
|
/// on every observed accept time. Anchoring on `accepted_at` folds each
|
||||||
|
/// send's wakeup latency into the next slot and then into every slot after
|
||||||
|
/// it, so the gate — and with it the whole group's send cadence — drifts
|
||||||
|
/// later without bound while the per-target schedules stay on their
|
||||||
|
/// absolute grid. The drift is not hypothetical: it shows up as a
|
||||||
|
/// monotonically growing `EchoSent::timer_error` and, once it exceeds one
|
||||||
|
/// interval, as probe slots the schedule then skips.
|
||||||
|
///
|
||||||
|
/// `accepted_at` is still the anchor in the two cases where continuing the
|
||||||
|
/// old cadence would be wrong: there is no previous gate, or the pacer has
|
||||||
|
/// fallen a full slot behind it. Re-anchoring there is what keeps a gate
|
||||||
|
/// stranded in the past from releasing a burst of catch-up sends once the
|
||||||
|
/// group starts moving again.
|
||||||
|
fn next_stagger_gate(&self, spacing: Duration, accepted_at: Instant) -> Option<Instant> {
|
||||||
|
let base = match self.send_gate {
|
||||||
|
Some(gate) if gate <= accepted_at && accepted_at.duration_since(gate) < spacing => gate,
|
||||||
|
_ => accepted_at,
|
||||||
|
};
|
||||||
|
base.checked_add(spacing)
|
||||||
|
}
|
||||||
|
|
||||||
fn record_stagger_acceptance(
|
fn record_stagger_acceptance(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: SendResult,
|
result: SendResult,
|
||||||
@@ -1626,7 +1650,7 @@ impl ManagedClientTask {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.last_stagger_send = Some(accepted_at);
|
self.last_stagger_send = Some(accepted_at);
|
||||||
self.send_gate = accepted_at.checked_add(spacing);
|
self.send_gate = self.next_stagger_gate(spacing, accepted_at);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
if let Some(observations) = &self.stagger_observations {
|
if let Some(observations) = &self.stagger_observations {
|
||||||
observations.lock().unwrap().push((_active, spacing));
|
observations.lock().unwrap().push((_active, spacing));
|
||||||
|
|||||||
@@ -1895,6 +1895,78 @@ fn stagger_gate_tracks_active_membership() {
|
|||||||
second.finish();
|
second.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The stagger gate must not fold each send's observed wakeup latency into
|
||||||
|
/// every later slot.
|
||||||
|
///
|
||||||
|
/// The gate is the binding constraint on when the *group* may send next (see
|
||||||
|
/// `next_deadline`), while each target's own schedule stays on an absolute
|
||||||
|
/// grid anchored at its open. Re-anchoring the gate on every observed accept
|
||||||
|
/// time therefore made the whole group's cadence drift later without bound:
|
||||||
|
/// `EchoSent::timer_error` grew monotonically, and once the drift exceeded one
|
||||||
|
/// interval `skip_missed_probe_slots_at` started dropping probe slots
|
||||||
|
/// outright. Measured before this was fixed, a single-target 20 s run at a
|
||||||
|
/// 50 ms interval sent 381 probes instead of 400.
|
||||||
|
///
|
||||||
|
/// This asserts the invariant directly rather than through a real-time run,
|
||||||
|
/// because reproducing accumulation behaviorally needs seconds of wall clock
|
||||||
|
/// and a jitter assumption. Two properties, both durable:
|
||||||
|
///
|
||||||
|
/// - while the pacer keeps up, N accepted sends advance the gate by exactly
|
||||||
|
/// N spacings from the first anchor, whatever lateness each send is
|
||||||
|
/// observed with;
|
||||||
|
/// - a gate the pacer has fallen a full spacing behind re-anchors on the
|
||||||
|
/// accept time instead, so a stranded gate cannot release a burst of
|
||||||
|
/// catch-up sends.
|
||||||
|
#[test]
|
||||||
|
fn the_stagger_gate_advances_by_one_spacing_per_send_without_accumulating_lateness() {
|
||||||
|
// No socket is involved: the gate is driven directly and the task is never
|
||||||
|
// polled, so an unreachable address is all a valid target set needs here.
|
||||||
|
let (mut task, _) = ManagedClient::task(
|
||||||
|
config(ManagedPacing::Staggered),
|
||||||
|
vec![target("only", "127.0.0.1:9".parse().unwrap())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spacing = Duration::from_millis(50);
|
||||||
|
let start = Instant::now();
|
||||||
|
let lateness = Duration::from_millis(3);
|
||||||
|
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
start,
|
||||||
|
);
|
||||||
|
assert_eq!(task.send_gate, Some(start + spacing), "first send anchors");
|
||||||
|
|
||||||
|
// Every later send is observed `lateness` after the gate that released it.
|
||||||
|
// The gate must still advance one spacing per send from `start`, not
|
||||||
|
// `spacing + lateness`.
|
||||||
|
for slot in 1..=8_u32 {
|
||||||
|
let released_at = start + spacing * slot;
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
released_at + lateness,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
task.send_gate,
|
||||||
|
Some(start + spacing * (slot + 1)),
|
||||||
|
"slot {slot} accumulated observed lateness into the gate"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A full spacing behind is no longer the same cadence, so the gate
|
||||||
|
// re-anchors rather than staying stranded in the past.
|
||||||
|
let stalled_gate = task.send_gate.expect("the gate is set");
|
||||||
|
let resumed_at = stalled_gate + spacing;
|
||||||
|
task.record_stagger_acceptance(
|
||||||
|
SendResult::Ready { accepted: true },
|
||||||
|
Some((1, spacing)),
|
||||||
|
resumed_at,
|
||||||
|
);
|
||||||
|
assert_eq!(task.send_gate, Some(resumed_at + spacing));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn timeout_discovery_inspects_at_most_one_budget_of_targets() {
|
fn timeout_discovery_inspects_at_most_one_budget_of_targets() {
|
||||||
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
let target_count = TIMEOUT_WORK_BUDGET + 1;
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ pub(crate) struct PendingProbe {
|
|||||||
/// eligible only for upstream one-way delay after local plausibility
|
/// eligible only for upstream one-way delay after local plausibility
|
||||||
/// validation against `tx_not_before_wall` (see `compute_one_way`'s
|
/// validation against `tx_not_before_wall` (see `compute_one_way`'s
|
||||||
/// `preferred_send_wall`). `sent_at` remains the userspace fallback for
|
/// `preferred_send_wall`). `sent_at` remains the userspace fallback for
|
||||||
/// upstream one-way delay when this is absent or implausible.
|
/// upstream one-way delay when this is absent, implausible, or kernel-ID
|
||||||
|
/// correlation was invalidated by a failed probe submission.
|
||||||
pub kernel_tx_timestamp: Option<SystemTime>,
|
pub kernel_tx_timestamp: Option<SystemTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,14 +12,17 @@
|
|||||||
//! After a successful Open, the client adapter best-effort upgrades the
|
//! After a successful Open, the client adapter best-effort upgrades the
|
||||||
//! socket from [`RX_TIMESTAMPING_FLAGS`] to [`error_queue::TX_TIMESTAMPING_FLAGS`]
|
//! socket from [`RX_TIMESTAMPING_FLAGS`] to [`error_queue::TX_TIMESTAMPING_FLAGS`]
|
||||||
//! via [`try_enable_tx_timestamping`]. When that succeeds, the automatic
|
//! via [`try_enable_tx_timestamping`]. When that succeeds, the automatic
|
||||||
//! `SOF_TIMESTAMPING_OPT_ID` counter the kernel assigns to each successfully
|
//! `SOF_TIMESTAMPING_OPT_ID` counter normally tracks the probe wire sequence
|
||||||
//! submitted datagram is used directly as the probe's wire sequence number
|
//! after successful sends, so it is used as a best-effort correlation ID.
|
||||||
//! (see the client crate's `AGENTS.md` for the correlation invariant this
|
//! A kernel ID can theoretically be consumed by a send that later fails,
|
||||||
//! relies on). [`drain_tx_timestamps`] performs a small bounded, nonblocking
|
//! desynchronizing that correlation. [`drain_tx_timestamps`] discards
|
||||||
//! read of `MSG_ERRQUEUE` so the adapter can opportunistically collect those
|
//! unmatched or implausible records; the userspace `sent_at` timestamp remains
|
||||||
//! timestamps without ever waiting for one. The observed kernel TX wall time
|
//! the fallback. After a probe send failure, correlation is disabled for that
|
||||||
//! is retained as metadata only; it is not consumed by any measurement in
|
//! session so later IDs cannot be misattributed. It performs a small bounded,
|
||||||
//! this change.
|
//! nonblocking read of
|
||||||
|
//! `MSG_ERRQUEUE` so the adapter can opportunistically collect timestamps
|
||||||
|
//! without ever waiting for one. The observed kernel TX wall time is retained
|
||||||
|
//! as metadata only; it is not consumed by any measurement in this change.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, IoSliceMut},
|
io::{self, IoSliceMut},
|
||||||
|
|||||||
@@ -624,22 +624,14 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: a live test proving that a failed nonblocking `sendmsg` does not
|
// NOTE: There is no live test for whether a failed nonblocking `sendmsg`
|
||||||
// consume an automatic OPT_ID is not included here. Forcing a reliable,
|
// consumes an automatic OPT_ID. Forcing a reliable, non-flaky `WouldBlock`
|
||||||
// non-flaky `WouldBlock` on a connected loopback UDP socket (e.g. via
|
// on a connected loopback UDP socket (for example through socket-buffer
|
||||||
// socket-buffer saturation) has no deterministic OS-level mechanism
|
// saturation) has no deterministic OS-level mechanism in this environment,
|
||||||
// available in this environment, and the repository's testing policy
|
// and the repository's testing policy forbids sleep/saturation-based
|
||||||
// explicitly forbids faking coverage with sleep/saturation-based
|
// flakiness. The correlation is intentionally best effort: a kernel ID can
|
||||||
// flakiness. That safety is instead established from Linux kernel
|
// theoretically be consumed by a send that later fails. The deterministic
|
||||||
// source: `_sock_tx_timestamp` (include/net/sock.h) and
|
// adapter test covers only the local half: an injected `WouldBlock` does
|
||||||
// `__ip_append_data`/`__ip6_append_data` (net/ipv4/ip_output.c,
|
// not advance the local wire-sequence counter. Any later unmatched kernel
|
||||||
// net/ipv6/ip6_output.c) increment `sk->sk_tskey` speculatively before
|
// timestamp is discarded and the userspace `sent_at` remains the fallback.
|
||||||
// a datagram is fully built and explicitly `atomic_dec` it back out on
|
|
||||||
// every error path (tracked via a local `hold_tskey` flag), so a failed
|
|
||||||
// send can never leave a gap in — or otherwise advance — the ID space
|
|
||||||
// the kernel hands out to successfully submitted datagrams. See the
|
|
||||||
// client crate's `AGENTS.md` for the full citation. The client-side
|
|
||||||
// half of this invariant (a `WouldBlock` `try_send` never advances the
|
|
||||||
// local wire-sequence counter used as the correlation ID) is covered by
|
|
||||||
// a deterministic test in the adapter module that injects `WouldBlock`.
|
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,7 @@ struct ActiveSession {
|
|||||||
pending: PendingMap,
|
pending: PendingMap,
|
||||||
timed_out: TimedOutMap,
|
timed_out: TimedOutMap,
|
||||||
completed: CompletedSet,
|
completed: CompletedSet,
|
||||||
|
kernel_tx_correlation_valid: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -423,15 +424,27 @@ impl SessionMachine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disable Linux kernel TX timestamp correlation after a probe send fails.
|
||||||
|
///
|
||||||
|
/// A failed submission can consume a kernel `SOF_TIMESTAMPING_OPT_ID`
|
||||||
|
/// without advancing the wire sequence. Existing associated timestamps
|
||||||
|
/// remain valid observations; later error-queue timestamps cannot be
|
||||||
|
/// associated safely and are ignored for the rest of this session.
|
||||||
|
pub(crate) fn invalidate_kernel_tx_correlation(&mut self) {
|
||||||
|
if let MachineState::Open(session) = &mut self.state {
|
||||||
|
session.kernel_tx_correlation_valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Record an observed Linux kernel TX timestamp for `wire_seq`, the
|
/// Record an observed Linux kernel TX timestamp for `wire_seq`, the
|
||||||
/// automatic `SOF_TIMESTAMPING_OPT_ID` the kernel assigned when the
|
/// automatic `SOF_TIMESTAMPING_OPT_ID` the kernel assigned when the
|
||||||
/// datagram was submitted.
|
/// datagram was submitted.
|
||||||
///
|
///
|
||||||
/// `wire_seq` doubles as that correlation ID: both counters start at
|
/// `wire_seq` normally matches that correlation ID after successful
|
||||||
/// zero for a session's first timestamped send and advance by exactly
|
/// sends. This is best effort rather than an invariant: a kernel ID can
|
||||||
/// one only on a confirmed successful submission, so they stay
|
/// theoretically be consumed by a send that later fails, leaving the
|
||||||
/// identical for the life of one open session (see the client crate's
|
/// counters desynchronized. An unmatched or implausible ID is discarded;
|
||||||
/// `AGENTS.md` for the full invariant).
|
/// the probe's userspace `sent_at` remains the fallback.
|
||||||
///
|
///
|
||||||
/// Updates a still-pending or already-timed-out probe in place. Never
|
/// Updates a still-pending or already-timed-out probe in place. Never
|
||||||
/// resurrects a completed, evicted, or unknown probe, and never touches
|
/// resurrects a completed, evicted, or unknown probe, and never touches
|
||||||
@@ -443,6 +456,9 @@ impl SessionMachine {
|
|||||||
let MachineState::Open(session) = &mut self.state else {
|
let MachineState::Open(session) = &mut self.state else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if !session.kernel_tx_correlation_valid {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if let Some(probe) = session.pending.get_mut(wire_seq) {
|
if let Some(probe) = session.pending.get_mut(wire_seq) {
|
||||||
probe.kernel_tx_timestamp.get_or_insert(timestamp);
|
probe.kernel_tx_timestamp.get_or_insert(timestamp);
|
||||||
return;
|
return;
|
||||||
@@ -636,6 +652,7 @@ impl SessionMachine {
|
|||||||
pending: PendingMap::new(self.config.max_pending_probes),
|
pending: PendingMap::new(self.config.max_pending_probes),
|
||||||
timed_out: TimedOutMap::new(self.config.max_pending_probes),
|
timed_out: TimedOutMap::new(self.config.max_pending_probes),
|
||||||
completed: CompletedSet::new(self.config.max_pending_probes),
|
completed: CompletedSet::new(self.config.max_pending_probes),
|
||||||
|
kernel_tx_correlation_valid: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let event = ClientEvent::SessionStarted {
|
let event = ClientEvent::SessionStarted {
|
||||||
@@ -1231,6 +1248,7 @@ mod tests {
|
|||||||
pending: PendingMap::new(max_pending_probes),
|
pending: PendingMap::new(max_pending_probes),
|
||||||
timed_out: TimedOutMap::new(max_pending_probes),
|
timed_out: TimedOutMap::new(max_pending_probes),
|
||||||
completed: CompletedSet::new(max_pending_probes),
|
completed: CompletedSet::new(max_pending_probes),
|
||||||
|
kernel_tx_correlation_valid: true,
|
||||||
}));
|
}));
|
||||||
machine
|
machine
|
||||||
}
|
}
|
||||||
@@ -2873,6 +2891,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalidated_kernel_tx_correlation_uses_userspace_send_time() {
|
||||||
|
let mono = Instant::now();
|
||||||
|
let mut machine = upstream_probe(send_wall_ms(1_000), mono);
|
||||||
|
|
||||||
|
// A failed send can consume a kernel ID without advancing wire_seq.
|
||||||
|
// A later timestamp whose ID happens to match a pending wire sequence
|
||||||
|
// must not be associated after that gap.
|
||||||
|
machine.invalidate_kernel_tx_correlation();
|
||||||
|
machine.record_kernel_tx_timestamp(0, send_wall_ms(1_005));
|
||||||
|
|
||||||
|
let events = upstream_reply(&mut machine, 1_020, send_wall_ms(1_040), mono);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reply_one_way(&events).unwrap().client_to_server,
|
||||||
|
Some(SignedDuration::from_nanos(20_000_000))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn upstream_one_way_delay_falls_back_without_kernel_tx() {
|
fn upstream_one_way_delay_falls_back_without_kernel_tx() {
|
||||||
let mono = Instant::now();
|
let mono = Instant::now();
|
||||||
@@ -3195,6 +3232,7 @@ mod tests {
|
|||||||
pending: PendingMap::new(MAX_PENDING_PROBES),
|
pending: PendingMap::new(MAX_PENDING_PROBES),
|
||||||
timed_out: TimedOutMap::new(MAX_PENDING_PROBES),
|
timed_out: TimedOutMap::new(MAX_PENDING_PROBES),
|
||||||
completed: CompletedSet::new(MAX_PENDING_PROBES),
|
completed: CompletedSet::new(MAX_PENDING_PROBES),
|
||||||
|
kernel_tx_correlation_valid: true,
|
||||||
}));
|
}));
|
||||||
(machine, params)
|
(machine, params)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -500,13 +500,14 @@ session policy lives.
|
|||||||
unmarked reply into a *dropped* one, because an unappliable marking stops the
|
unmarked reply into a *dropped* one, because an unappliable marking stops the
|
||||||
send. Verify a platform before extending that list.
|
send. Verify a platform before extending that list.
|
||||||
- A genuinely dual-stack `[::]` listener still emits **unmarked** IPv4-mapped
|
- A genuinely dual-stack `[::]` listener still emits **unmarked** IPv4-mapped
|
||||||
replies, and this is known. It is not fixed by choosing the option per reply,
|
replies on Linux. When reply DSCP/traffic-class marking for IPv4 peers
|
||||||
for the macOS reason above, and it predates wildcard source selection. Leave it
|
matters, configure the normal same-port `[::]:PORT` plus `0.0.0.0:PORT`
|
||||||
until it is worth its own slice, and do not "fix" it by weakening the rule that
|
listener pair instead; `ServerSet` makes the IPv6 half IPv6-only for that
|
||||||
an unappliable marking stops the send. The crate forbids unsafe code;
|
pair. Do not force `IPV6_V6ONLY` on every lone IPv6 listener, and do not
|
||||||
there is no raw `setsockopt` and no `sendmsg`/`cmsg` machinery. Keep the
|
weaken the rule that an unappliable marking stops the send. The crate forbids
|
||||||
target lists identical to the pinned `socket2` version's own gates — check
|
unsafe code; there is no raw `setsockopt` and no `sendmsg`/`cmsg` machinery.
|
||||||
its source rather than copying another crate's older matrix.
|
Keep the target lists identical to the pinned `socket2` version's own gates
|
||||||
|
— check its source rather than copying another crate's older matrix.
|
||||||
|
|
||||||
## Rate, lifetime and server-initiated close
|
## Rate, lifetime and server-initiated close
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ impl ServerSet {
|
|||||||
/// An IPv4-mapped address such as `[::ffff:0.0.0.0]` is an IPv4 listener
|
/// An IPv4-mapped address such as `[::ffff:0.0.0.0]` is an IPv4 listener
|
||||||
/// here, not an IPv6 one, matching how the runtime already reads it.
|
/// here, not an IPv6 one, matching how the runtime already reads it.
|
||||||
///
|
///
|
||||||
|
/// A lone explicit dual-stack `[::]:PORT` listener is not the recommended
|
||||||
|
/// configuration when reply DSCP/traffic-class marking for IPv4 peers
|
||||||
|
/// matters: Linux serves those peers as IPv4-mapped and cannot retain the
|
||||||
|
/// negotiated marking on their replies. Request the normal same-port
|
||||||
|
/// `[::]:PORT` and `0.0.0.0:PORT` pair instead. This method makes the IPv6
|
||||||
|
/// half IPv6-only for that pair only; a lone IPv6 listener retains its
|
||||||
|
/// normal socket behavior.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// Returns [`ServerSetError::NoListeners`] if `addrs` is empty — a set
|
/// Returns [`ServerSetError::NoListeners`] if `addrs` is empty — a set
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ The build also produces dedicated binaries: `irtt-client`, `irtt-tui`, and
|
|||||||
what it is invoked or copied as — it does not consult argv0 dispatch logic.
|
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
|
`irtt-rs` itself inspects argv0 first: if invoked (or symlinked/copied) under
|
||||||
one of the recognized applet names — `irtt-client`, `irtt-tui`, `irttd`, or
|
one of the recognized applet names — `irtt-client`, `irtt-tui`, or
|
||||||
`irtt-server` — it runs that applet directly, without needing a subcommand.
|
`irtt-server` — it runs that applet directly, without needing a subcommand.
|
||||||
Any other name starting with `irtt-` (other than `irtt-rs` itself) is
|
Any other name starting with `irtt-` (other than `irtt-rs` itself) is
|
||||||
rejected as an unknown applet name before subcommand parsing is even
|
rejected as an unknown applet name before subcommand parsing is even
|
||||||
|
|||||||
+40
-1
@@ -60,7 +60,17 @@ served.
|
|||||||
A port of `0` selects an unused port per listener, so two such binds get two
|
A port of `0` selects an unused port per listener, so two such binds get two
|
||||||
different ports.
|
different ports.
|
||||||
|
|
||||||
With no `--bind` at all, see [Default bind](#default-bind) above.
|
With no `--bind` at all, see [Description](#description) above for the
|
||||||
|
default wildcard pair and the address-family fallback it allows.
|
||||||
|
|
||||||
|
When reply DSCP/traffic-class marking for IPv4 peers matters, do not configure
|
||||||
|
one explicit dual-stack `[::]:PORT` listener by itself. On Linux, IPv4 peers
|
||||||
|
served through that socket are IPv4-mapped and their replies cannot retain the
|
||||||
|
negotiated marking. Configure the ordinary paired `[::]:PORT` and
|
||||||
|
`0.0.0.0:PORT` listeners instead; the IPv6 listener is made IPv6-only for that
|
||||||
|
same-port pair, so each family uses the correct socket option. This is a
|
||||||
|
configuration workaround, not a reason to force `IPV6_V6ONLY` for every lone
|
||||||
|
IPv6 listener.
|
||||||
|
|
||||||
### `--hmac KEY`
|
### `--hmac KEY`
|
||||||
|
|
||||||
@@ -136,6 +146,35 @@ and they are sent unmarked. The session is not refused.
|
|||||||
Durations everywhere in this command accept an integer followed by a unit:
|
Durations everywhere in this command accept an integer followed by a unit:
|
||||||
`ms`, `s`, or `m`.
|
`ms`, `s`, or `m`.
|
||||||
|
|
||||||
|
## ENVIRONMENT
|
||||||
|
|
||||||
|
Every option above can also be supplied through an environment variable, for
|
||||||
|
deployments that configure by environment rather than by command line
|
||||||
|
(containers, orchestrators, unit files). A flag given on the command line
|
||||||
|
overrides the corresponding variable entirely; an unset variable leaves the
|
||||||
|
built-in default in place.
|
||||||
|
|
||||||
|
| Variable | Option |
|
||||||
|
| --- | --- |
|
||||||
|
| `IRTT_SERVER_BIND` | `--bind`, one address only |
|
||||||
|
| `IRTT_SERVER_HMAC` | `--hmac` |
|
||||||
|
| `IRTT_SERVER_MAX_SESSIONS` | `--max-sessions` |
|
||||||
|
| `IRTT_SERVER_MAX_PACKET_LENGTH` | `--max-packet-length` |
|
||||||
|
| `IRTT_SERVER_MIN_INTERVAL` | `--min-interval` |
|
||||||
|
| `IRTT_SERVER_BURST` | `--burst` |
|
||||||
|
| `IRTT_SERVER_IDLE_TIMEOUT` | `--idle-timeout` |
|
||||||
|
| `IRTT_SERVER_MAX_DURATION` | `--max-duration` |
|
||||||
|
| `IRTT_SERVER_TIMESTAMP_ALLOWANCE` | `--timestamp-allowance` |
|
||||||
|
| `IRTT_SERVER_NO_DSCP` | `--no-dscp` |
|
||||||
|
|
||||||
|
`IRTT_SERVER_BIND` takes exactly one address, the same as a single `--bind`;
|
||||||
|
there is no way to request several listeners from the environment. Serving
|
||||||
|
more than one address needs repeated `--bind` options.
|
||||||
|
|
||||||
|
`IRTT_SERVER_NO_DSCP` is the only boolean. `0`, `false`, `no`, `n`, `off` and
|
||||||
|
an empty value leave traffic-class marking enabled; any other value disables
|
||||||
|
it, and an unset variable leaves it enabled.
|
||||||
|
|
||||||
## OUTPUT
|
## OUTPUT
|
||||||
|
|
||||||
Each bound endpoint is printed on startup, once every listener is up, which
|
Each bound endpoint is printed on startup, once every listener is up, which
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ presentation bounds on top of the client's own statistics retention (see
|
|||||||
total memory is dominated by whichever of the two is larger for your target
|
total memory is dominated by whichever of the two is larger for your target
|
||||||
count.
|
count.
|
||||||
|
|
||||||
|
A **finite** `--duration` TUI run retains exact statistics samples exactly as
|
||||||
|
`irtt-client(1)` does in finite mode, so its retained state grows with the
|
||||||
|
probe count rather than approaching a bound. Unlike `irtt-client`, the TUI
|
||||||
|
prints **no** memory warning when that estimate gets large: it owns the
|
||||||
|
alternate screen, and a stderr warning there would be invisible or would
|
||||||
|
corrupt the display. Size a long finite TUI run from the figures in
|
||||||
|
`irtt-client(1)` MEASUREMENTS AND MEMORY, or use the continuous default.
|
||||||
|
|
||||||
## EXIT BEHAVIOR
|
## EXIT BEHAVIOR
|
||||||
|
|
||||||
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. Otherwise,
|
Quitting with `q` or `Ctrl-C` is an interrupted, successful exit. Otherwise,
|
||||||
|
|||||||
Generated
+127
@@ -8,6 +8,18 @@ version = "1.4.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -35,6 +47,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cmov"
|
name = "cmov"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
@@ -126,10 +144,24 @@ dependencies = [
|
|||||||
name = "irtt-proto-fuzz"
|
name = "irtt-proto-fuzz"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
"irtt-proto",
|
"irtt-proto",
|
||||||
|
"irtt-server",
|
||||||
"libfuzzer-sys",
|
"libfuzzer-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "irtt-server"
|
||||||
|
version = "0.5.2"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"irtt-proto",
|
||||||
|
"nix",
|
||||||
|
"socket2",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jobserver"
|
name = "jobserver"
|
||||||
version = "0.1.35"
|
version = "0.1.35"
|
||||||
@@ -166,6 +198,45 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memoffset"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nix"
|
||||||
|
version = "0.31.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if",
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"memoffset",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.107"
|
version = "1.0.107"
|
||||||
@@ -196,6 +267,16 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -233,6 +314,31 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.20.1"
|
version = "1.20.1"
|
||||||
@@ -244,3 +350,24 @@ name = "unicode-ident"
|
|||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
@@ -8,11 +8,15 @@ edition = "2021"
|
|||||||
cargo-fuzz = true
|
cargo-fuzz = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
arbitrary = "1.4.2"
|
||||||
libfuzzer-sys = "0.4.13"
|
libfuzzer-sys = "0.4.13"
|
||||||
|
|
||||||
[dependencies.irtt-proto]
|
[dependencies.irtt-proto]
|
||||||
path = "../crates/irtt-proto"
|
path = "../crates/irtt-proto"
|
||||||
|
|
||||||
|
[dependencies.irtt-server]
|
||||||
|
path = "../crates/irtt-server"
|
||||||
|
|
||||||
# Use independent workspace for fuzzers
|
# Use independent workspace for fuzzers
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["."]
|
members = ["."]
|
||||||
@@ -51,3 +55,10 @@ path = "fuzz_targets/decode_varint.rs"
|
|||||||
test = false
|
test = false
|
||||||
doc = false
|
doc = false
|
||||||
bench = false
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "server_core_datagrams"
|
||||||
|
path = "fuzz_targets/server_core_datagrams.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
+9
-6
@@ -1,6 +1,7 @@
|
|||||||
# `irtt-proto` fuzzing
|
# `irtt-rs` fuzzing
|
||||||
|
|
||||||
Coverage-guided fuzzing of `irtt-proto`'s wire decoders via
|
Coverage-guided fuzzing of `irtt-proto` wire decoders and `irtt-server`'s public
|
||||||
|
`ServerCore::handle_datagram` API via
|
||||||
[`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)/libFuzzer.
|
[`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)/libFuzzer.
|
||||||
|
|
||||||
This project is intentionally isolated from the normal stable/MSRV
|
This project is intentionally isolated from the normal stable/MSRV
|
||||||
@@ -22,14 +23,16 @@ of the root workspace, requires nightly Rust, and is not built by
|
|||||||
| `decode_echo_reply` | `decode_echo_reply` against a deterministic, bounded `Params` matrix, unauthenticated and HMAC-authenticated |
|
| `decode_echo_reply` | `decode_echo_reply` against a deterministic, bounded `Params` matrix, unauthenticated and HMAC-authenticated |
|
||||||
| `decode_params` | `Params::decode` and `Params::decode_with_presence`, plus an encode/decode round trip |
|
| `decode_params` | `Params::decode` and `Params::decode_with_presence`, plus an encode/decode round trip |
|
||||||
| `decode_varint` | `varint::decode_uvarint`/`decode_varint` and their round trip through `encode_uvarint`/`encode_varint` |
|
| `decode_varint` | `varint::decode_uvarint`/`decode_varint` and their round trip through `encode_uvarint`/`encode_varint` |
|
||||||
|
| `server_core_datagrams` | A bounded mix of raw datagrams and production-encoded Open/Echo/Close requests against one `ServerCore`; it varies IPv4/IPv6 peers, session configuration, and bounded (up to 128-byte) HMAC keys, authenticates keyed requests, reuses issued tokens, and asserts the session cap |
|
||||||
|
|
||||||
The invariant every target asserts is: **arbitrary bytes must never panic**.
|
The invariant every target asserts is: **arbitrary bytes must never panic**.
|
||||||
Returning `Err` is normal and expected; a successful decode is also normal.
|
Returning `Err` is normal and expected; a successful decode is also normal.
|
||||||
|
|
||||||
Every target bounds its input to 128 KiB (`MAX_INPUT_LEN` in each harness) so
|
Every target bounds its input (`MAX_INPUT_LEN` in each harness) so libFuzzer
|
||||||
libFuzzer spends its time exploring protocol structure rather than
|
spends its time exploring protocol structure rather than allocating/copying
|
||||||
allocating/copying megabytes — production IRTT datagrams are UDP-sized, well
|
megabytes. `server_core_datagrams` additionally caps its sequence length and
|
||||||
under this bound.
|
each individual datagram and configures the core to the same packet cap, so one
|
||||||
|
libFuzzer input cannot become a tiny DoS benchmark.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use arbitrary::Unstructured;
|
||||||
|
use irtt_proto::{decode_open_reply, encode_request, Params, RequestToEncode};
|
||||||
|
use irtt_server::{ServerConfig, ServerCore};
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
/// Bounds one libFuzzer input as well as the work it can ask the core to do.
|
||||||
|
/// These are deliberately much smaller than a production UDP payload: the
|
||||||
|
/// target is stateful parser/session exploration, not packet-size throughput.
|
||||||
|
const MAX_INPUT_LEN: usize = 32 * 1024;
|
||||||
|
const MAX_DATAGRAMS: usize = 16;
|
||||||
|
const MAX_DATAGRAM_LEN: usize = 2 * 1024;
|
||||||
|
const MAX_SESSIONS: u8 = 8;
|
||||||
|
const MAX_HMAC_KEY_LEN: usize = 128;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FuzzSession {
|
||||||
|
token: u64,
|
||||||
|
peer: SocketAddr,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if data.len() > MAX_INPUT_LEN {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut input = Unstructured::new(data);
|
||||||
|
let Ok(mut config) = config(&mut input) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let hmac_key = match input.arbitrary::<bool>() {
|
||||||
|
Ok(true) => {
|
||||||
|
let Ok(length) = input.int_in_range(1..=MAX_HMAC_KEY_LEN) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
input.bytes(length).ok()
|
||||||
|
}
|
||||||
|
Ok(false) => None,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
if let Some(key) = hmac_key {
|
||||||
|
config = config.with_hmac_key(key);
|
||||||
|
}
|
||||||
|
let mut core = ServerCore::new(config);
|
||||||
|
let mut sessions = Vec::new();
|
||||||
|
|
||||||
|
for _ in 0..MAX_DATAGRAMS {
|
||||||
|
let Ok(operation) = input.arbitrary::<u8>() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
match operation % 4 {
|
||||||
|
// Keep arbitrary-byte admission coverage alongside the stateful
|
||||||
|
// production-encoded requests below.
|
||||||
|
0 => raw_datagram(&mut core, &mut input),
|
||||||
|
1 => open_session(&mut core, &mut input, hmac_key, &mut sessions),
|
||||||
|
2 => echo_session(&mut core, &mut input, hmac_key, &sessions),
|
||||||
|
_ => close_session(&mut core, &mut input, hmac_key, &mut sessions),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbitrary network input is expected to be rejected or to produce an
|
||||||
|
// internal error in exceptional host conditions. It must never panic,
|
||||||
|
// and it may never grow the bounded table past its configured cap.
|
||||||
|
assert!(core.session_count() <= core.config().max_sessions());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn config(input: &mut Unstructured<'_>) -> arbitrary::Result<ServerConfig> {
|
||||||
|
let max_sessions = usize::from(input.int_in_range(0..=MAX_SESSIONS)?);
|
||||||
|
Ok(ServerConfig::default()
|
||||||
|
.with_max_sessions(max_sessions)
|
||||||
|
.with_max_packet_length(MAX_DATAGRAM_LEN)
|
||||||
|
.with_min_send_interval(Duration::ZERO))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_datagram(core: &mut ServerCore, input: &mut Unstructured<'_>) {
|
||||||
|
let (Ok(peer), Ok(length)) = (peer(input), input.int_in_range(0..=MAX_DATAGRAM_LEN)) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(packet) = input.bytes(length) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let _ = core.handle_datagram(peer, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_session(
|
||||||
|
core: &mut ServerCore,
|
||||||
|
input: &mut Unstructured<'_>,
|
||||||
|
hmac_key: Option<&[u8]>,
|
||||||
|
sessions: &mut Vec<FuzzSession>,
|
||||||
|
) {
|
||||||
|
let Ok(peer) = peer(input) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let params = session_params();
|
||||||
|
let request = encode_request(
|
||||||
|
RequestToEncode::Open {
|
||||||
|
params: ¶ms,
|
||||||
|
no_test: false,
|
||||||
|
},
|
||||||
|
hmac_key,
|
||||||
|
)
|
||||||
|
.expect("fixed valid fuzz open must encode");
|
||||||
|
let Ok(Some(reply)) = core.handle_datagram(peer, &request) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok(reply) = decode_open_reply(reply.bytes(), hmac_key) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
sessions.push(FuzzSession {
|
||||||
|
token: reply.token,
|
||||||
|
peer,
|
||||||
|
params: reply.params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn echo_session(
|
||||||
|
core: &mut ServerCore,
|
||||||
|
input: &mut Unstructured<'_>,
|
||||||
|
hmac_key: Option<&[u8]>,
|
||||||
|
sessions: &[FuzzSession],
|
||||||
|
) {
|
||||||
|
let Some(session) =
|
||||||
|
sessions.get(usize::from(input.arbitrary::<u8>().unwrap_or(0)) % sessions.len().max(1))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let sequence = input.arbitrary().unwrap_or_default();
|
||||||
|
let request = encode_request(
|
||||||
|
RequestToEncode::Echo {
|
||||||
|
token: session.token,
|
||||||
|
sequence,
|
||||||
|
params: &session.params,
|
||||||
|
payload: &[],
|
||||||
|
},
|
||||||
|
hmac_key,
|
||||||
|
)
|
||||||
|
.expect("fixed valid fuzz echo must encode");
|
||||||
|
let _ = core.handle_datagram(session.peer, &request);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_session(
|
||||||
|
core: &mut ServerCore,
|
||||||
|
input: &mut Unstructured<'_>,
|
||||||
|
hmac_key: Option<&[u8]>,
|
||||||
|
sessions: &mut Vec<FuzzSession>,
|
||||||
|
) {
|
||||||
|
let index = usize::from(input.arbitrary::<u8>().unwrap_or(0)) % sessions.len().max(1);
|
||||||
|
let Some(session) = sessions.get(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let request = encode_request(
|
||||||
|
RequestToEncode::Close {
|
||||||
|
token: session.token,
|
||||||
|
},
|
||||||
|
hmac_key,
|
||||||
|
)
|
||||||
|
.expect("fixed valid fuzz close must encode");
|
||||||
|
let _ = core.handle_datagram(session.peer, &request);
|
||||||
|
sessions.swap_remove(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_params() -> Params {
|
||||||
|
Params {
|
||||||
|
protocol_version: 1,
|
||||||
|
length: 256,
|
||||||
|
..Params::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peer(input: &mut Unstructured<'_>) -> arbitrary::Result<SocketAddr> {
|
||||||
|
let port = input.arbitrary()?;
|
||||||
|
let address = if input.arbitrary()? {
|
||||||
|
IpAddr::V4(Ipv4Addr::from(input.arbitrary::<[u8; 4]>()?))
|
||||||
|
} else {
|
||||||
|
IpAddr::V6(Ipv6Addr::from(input.arbitrary::<[u8; 16]>()?))
|
||||||
|
};
|
||||||
|
Ok(SocketAddr::new(address, port))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user