From db5b6b10bd475eb9ab200511bee9e175b5d9846f Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 13:37:16 +0000 Subject: [PATCH 01/12] config: unify default control-socket path resolution Daemon and client tools previously evaluated the same three locations (`/run/fips`, `XDG_RUNTIME_DIR`, `/tmp`) in different orders, allowing fipsctl/fipstop to connect to a socket the daemon never bound when neither side set `node.control.socket_path` explicitly. Collapse the three call sites (`default_control_path`, `default_gateway_path`, `ControlConfig::default_socket_path`) into a shared `resolve_default_socket` helper. Canonical order is `/run/fips` -> `$XDG_RUNTIME_DIR/fips/` -> `/tmp/fips-`. Two hardening fixes folded in: writability is probed via tempfile create rather than mode bits (ACL- and group-aware), and `XDG_RUNTIME_DIR` is validated as an existing directory before being used (avoids stale post-logout values). The deployed fleet is unaffected -- packaged configs set `node.control.socket_path` explicitly. The fix surfaces for dev runs and the binary-install getting-started path. --- CHANGELOG.md | 10 ++ src/config/mod.rs | 205 +++++++++++++++++++++++++++++++++++++---- src/config/node.rs | 17 ++-- src/gateway/control.rs | 8 ++ 4 files changed, 210 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8135752..b4ad40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -290,6 +290,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Default control-socket path resolution: daemon and client tools now + use a shared resolver, eliminating a divergence where `fipsctl` / + `fipstop` could connect to a socket the daemon never bound (notably + on dev runs with `XDG_RUNTIME_DIR` set, or after a prior packaged + install left a root-owned `/run/fips` behind). Canonical order is + `/run/fips` → `$XDG_RUNTIME_DIR/fips/` → `/tmp/fips-`, with + writability of `/run/fips` probed via tempfile create (ACL- and + group-aware) and `XDG_RUNTIME_DIR` validated as an existing + directory before being used. The deployed fleet is unaffected: + packaged configs set `node.control.socket_path` explicitly. - UDP transport with `advertise_on_nostr: true` + `public: true` + a wildcard `bind_addr` (e.g. `0.0.0.0:2121`) is now advertised with its STUN-discovered public IPv4 instead of being silently diff --git a/src/config/mod.rs b/src/config/mod.rs index e8e6b6f..ec563ee 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -90,21 +90,77 @@ pub fn pub_file_path(config_path: &Path) -> PathBuf { .join(PUB_FILENAME) } +/// Resolve a default Unix-socket path under the canonical order: +/// `/run/fips/` → `$XDG_RUNTIME_DIR/fips/` → `/tmp/fips-`. +/// +/// `/run/fips` is the packaged convention (`root:fips 0770` directory created +/// by the daemon at bind time). `XDG_RUNTIME_DIR` covers non-root dev runs +/// where `/run/fips` does not exist or is not writable. `/tmp` is the +/// last-resort fallback. +/// +/// Hardening notes: +/// - `/run/fips` is accepted only if the directory exists and is writable by +/// the current process. `create_dir_all` reporting `Ok(())` is *not* +/// sufficient: it returns `Ok` for an existing root-owned dir that we +/// cannot write to, which would silently steer a non-root daemon onto a +/// path that fails at bind time. Writability is probed via tempfile create +/// rather than mode bits so ACLs and group membership (the dir is +/// `root:fips 0770`) are honored. +/// - `XDG_RUNTIME_DIR` is validated as an existing directory before being +/// used; a stale post-logout value (after `pam_systemd` reaps the dir) is +/// treated as missing. +#[cfg(unix)] +pub(crate) fn resolve_default_socket(filename: &str) -> String { + // 1. /run/fips — accept only if the directory exists and is writable. + let run_fips = Path::new("/run/fips"); + if run_fips.is_dir() && is_writable_dir(run_fips) { + return format!("/run/fips/{filename}"); + } + // Also accept /run/fips if we can create it (covers the first-boot + // daemon-as-root case before the directory has been materialized). The + // actual chown happens at bind time. + if std::fs::create_dir_all(run_fips).is_ok() && is_writable_dir(run_fips) { + return format!("/run/fips/{filename}"); + } + + // 2. $XDG_RUNTIME_DIR/fips/ — only if the variable points at an existing + // directory. + if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") { + let xdg_path = Path::new(&xdg); + if xdg_path.is_dir() { + return format!("{xdg}/fips/{filename}"); + } + } + + // 3. Last resort: /tmp with a name-mangled prefix so multiple users + // don't collide. + format!("/tmp/fips-{filename}") +} + +#[cfg(unix)] +fn is_writable_dir(path: &Path) -> bool { + // Probe via tempfile creation rather than mode bits: mode-bit checks miss + // ACLs and group-membership effects (the /run/fips dir is `root:fips + // 0770` and the daemon may run as a user that's in the `fips` group). + let probe = path.join(format!(".fips-write-probe-{}", std::process::id())); + match std::fs::File::create(&probe) { + Ok(_) => { + let _ = std::fs::remove_file(&probe); + true + } + Err(_) => false, + } +} + /// Default control socket path for fipsctl / fipstop. /// -/// On Unix, checks the system-wide path first (used when the daemon runs as -/// a systemd service), then falls back to the user's XDG runtime directory. -/// On Windows, returns the default TCP port ("21210"). +/// On Unix, delegates to [`resolve_default_socket`] for the canonical +/// `/run/fips` → `XDG_RUNTIME_DIR` → `/tmp` order. On Windows, returns the +/// default TCP port ("21210"). pub fn default_control_path() -> PathBuf { #[cfg(unix)] { - if Path::new("/run/fips").exists() { - PathBuf::from("/run/fips/control.sock") - } else if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { - PathBuf::from(format!("{runtime_dir}/fips/control.sock")) - } else { - PathBuf::from("/tmp/fips-control.sock") - } + PathBuf::from(resolve_default_socket("control.sock")) } #[cfg(windows)] { @@ -114,18 +170,16 @@ pub fn default_control_path() -> PathBuf { /// Default gateway control socket path. /// -/// On Unix, follows the same pattern as the main control socket. -/// On Windows, returns a placeholder TCP port ("21211"). +/// On Unix, delegates to [`resolve_default_socket`] (same canonical order as +/// the main control socket). The gateway daemon itself uses a hardcoded +/// `/run/fips/gateway.sock` since gateway operation requires root for +/// NAT/conntrack management; this client-side resolver falls through +/// gracefully for non-root dev runs that need a gateway socket path. On +/// Windows, returns a placeholder TCP port ("21211"). pub fn default_gateway_path() -> PathBuf { #[cfg(unix)] { - if Path::new("/run/fips").exists() { - PathBuf::from("/run/fips/gateway.sock") - } else if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { - PathBuf::from(format!("{runtime_dir}/fips/gateway.sock")) - } else { - PathBuf::from("/tmp/fips-gateway.sock") - } + PathBuf::from(resolve_default_socket("gateway.sock")) } #[cfg(windows)] { @@ -1449,4 +1503,117 @@ peers: let cfg = UdpConfig::default(); assert!(cfg.accept_connections()); } + + /// Mutex serializing tests that mutate `XDG_RUNTIME_DIR`. `cargo test` + /// runs tests on multiple threads in the same process, and env mutation + /// is process-global, so concurrent env-touching tests would race. + #[cfg(unix)] + static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + #[cfg(unix)] + #[test] + fn test_resolve_default_socket_call_sites_agree() { + // The three resolver call sites must all produce strings that agree + // on the directory, differing only in the filename suffix. + let _g = ENV_MUTEX.lock().unwrap(); + + let control_client = default_control_path().to_string_lossy().into_owned(); + let gateway_client = default_gateway_path().to_string_lossy().into_owned(); + let control_daemon = ControlConfig::default().socket_path; + + // Daemon-side and client-side control paths must be identical. + assert_eq!( + control_daemon, control_client, + "daemon and client default control-socket paths diverged: \ + daemon={control_daemon}, client={control_client}" + ); + + // Control and gateway must share a parent directory (or /tmp prefix). + let control_dir = std::path::Path::new(&control_client) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default(); + let gateway_dir = std::path::Path::new(&gateway_client) + .parent() + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or_default(); + assert_eq!( + control_dir, gateway_dir, + "control and gateway default-socket paths picked different directories: \ + control={control_client}, gateway={gateway_client}" + ); + } + + #[cfg(unix)] + #[test] + fn test_resolve_default_socket_xdg_when_no_run_fips() { + // With /run/fips unwritable (non-root tests) and XDG_RUNTIME_DIR + // pointing at an existing directory, the resolver picks XDG. + let _g = ENV_MUTEX.lock().unwrap(); + + let temp_dir = TempDir::new().unwrap(); + let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok(); + // SAFETY: serialized via ENV_MUTEX above. + unsafe { + std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path()); + } + + let path = resolve_default_socket("control.sock"); + + // Restore env before asserting so a panic doesn't leak state. + unsafe { + match prev_xdg { + Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v), + None => std::env::remove_var("XDG_RUNTIME_DIR"), + } + } + + // If /run/fips happens to be writable in the test environment (CI + // running as root, for instance), the resolver legitimately picks + // /run/fips and skips XDG entirely. Accept either outcome but + // demand that one of the two canonical prefixes is chosen — never + // /tmp when XDG was valid. + assert!( + path.starts_with("/run/fips/") + || path.starts_with(&format!("{}/fips/", temp_dir.path().display())), + "expected /run/fips or XDG path, got: {path}" + ); + } + + #[cfg(unix)] + #[test] + fn test_resolve_default_socket_tmp_when_xdg_invalid() { + // With XDG_RUNTIME_DIR pointing at a non-existent directory and + // /run/fips unwritable, the resolver falls through to /tmp. + let _g = ENV_MUTEX.lock().unwrap(); + + let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok(); + // Use a path that definitely does not exist. + let bogus = "/nonexistent-xdg-runtime-dir-for-fips-test-zzz"; + // SAFETY: serialized via ENV_MUTEX. + unsafe { + std::env::set_var("XDG_RUNTIME_DIR", bogus); + } + + let path = resolve_default_socket("gateway.sock"); + + unsafe { + match prev_xdg { + Some(v) => std::env::set_var("XDG_RUNTIME_DIR", v), + None => std::env::remove_var("XDG_RUNTIME_DIR"), + } + } + + // Accept either /run/fips/ (test running as root with that dir + // writable) or /tmp/fips-... (the dev-machine fallback). Never + // accept the bogus XDG dir leaking through. + assert!( + path.starts_with("/run/fips/") || path == "/tmp/fips-gateway.sock", + "expected /run/fips or /tmp fallback, got: {path}" + ); + assert!( + !path.starts_with(bogus), + "stale/invalid XDG_RUNTIME_DIR leaked into resolver: {path}" + ); + } } diff --git a/src/config/node.rs b/src/config/node.rs index 5eae6ef..0f6421f 100644 --- a/src/config/node.rs +++ b/src/config/node.rs @@ -780,20 +780,15 @@ impl ControlConfig { /// Default control socket path. /// - /// On Unix, returns a Unix domain socket path (XDG_RUNTIME_DIR, /run/fips, - /// or /tmp fallback). On Windows, returns a TCP port number as a string - /// since Windows does not support Unix domain sockets; the control socket - /// listens on localhost at this port. + /// On Unix, delegates to [`super::resolve_default_socket`] for the + /// canonical `/run/fips` → `XDG_RUNTIME_DIR` → `/tmp` order shared with + /// the client-side `default_control_path`. On Windows, returns a TCP + /// port number as a string since Windows does not support Unix domain + /// sockets; the control socket listens on localhost at this port. fn default_socket_path() -> String { #[cfg(unix)] { - if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { - format!("{runtime_dir}/fips/control.sock") - } else if std::fs::create_dir_all("/run/fips").is_ok() { - "/run/fips/control.sock".to_string() - } else { - "/tmp/fips-control.sock".to_string() - } + super::resolve_default_socket("control.sock") } #[cfg(windows)] { diff --git a/src/gateway/control.rs b/src/gateway/control.rs index 7bd8ce8..a77014d 100644 --- a/src/gateway/control.rs +++ b/src/gateway/control.rs @@ -15,6 +15,14 @@ use tokio::sync::watch; use tracing::{debug, info, warn}; /// Socket path for the gateway control socket. +/// +/// Hardcoded to `/run/fips/gateway.sock`: gateway operation requires root +/// for NAT/conntrack management, so the daemon side never needs to fall +/// back to `XDG_RUNTIME_DIR` or `/tmp`. Client tools resolve the path via +/// [`crate::config::default_gateway_path`], which uses the shared +/// [`crate::config::resolve_default_socket`] helper and falls through to +/// `XDG_RUNTIME_DIR` / `/tmp` for non-root dev runs that don't have +/// `/run/fips` writable. pub const GATEWAY_SOCKET_PATH: &str = "/run/fips/gateway.sock"; /// Maximum request size in bytes (4 KB). From 9112c8f7f0273e48cc9516383ba2c8cfcc05d5ab Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 13:45:30 +0000 Subject: [PATCH 02/12] changelog: prep [Unreleased] for v0.3.0 - Add a Documentation entry covering the docs/ reorganisation, top-level getting-started.md, per-section landing pages, source-accuracy pass, and gateway feature-set rewrite. - Add a Fixed entry covering propagation of spanning-tree updates whose changes are confined to internal path edges (no root or depth delta). - Add a single rolled-up entry covering expanded test coverage across the new-feature surface plus CI hardening. - Drop a tree-ancestry test-determinism bullet that did not change user-visible behaviour. --- CHANGELOG.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4ad40d..5fa8f99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -287,6 +287,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `max_fpr` and returns `Option`, returning `None` for saturated filters; this propagates through `compute_mesh_size` into `estimated_mesh_size` (already `Option`) +- The `docs/` tree is reorganised so readers can find content by + what they're trying to do: tutorials for new users, how-to guides + for specific tasks, reference material for configuration and + protocol details, and design discussion for architectural + background. New top-level `getting-started.md` and per-section + landing pages anchor the entry points. Content was reconciled + against current source: protocol layer details, wire-format + diagrams, configuration knobs, and CLI references were brought + back into agreement with the implementation. Gateway feature-set + documentation was rewritten end-to-end. +- Test coverage was substantially expanded for the new release + surface (discovery state machine, control-socket query handlers, + decrypt-failure thresholds, STUN parser, gateway, NAT traversal, + packaging install paths) alongside CI-side hardening for the new + Windows and macOS platforms. ### Fixed @@ -450,14 +465,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 tree specification. The receive path now verifies that the ancestry is structurally consistent with the signed parent declaration before mutating tree state. -- Make the tree ancestry acceptance unit test deterministic. - `test_tree_announce_validate_semantics_accepts_valid_non_root` - generated a random signing identity while pinning the fixed root - to `node_addr[0] = 0x01`; about 2 in 256 random identities were - numerically smaller than the claimed root, triggering - `AncestryRootNotMinimum`. The test now regenerates the identity - until its `node_addr` is strictly larger than both the fixed - parent and root. +- Spanning-tree updates that change only the internal path between + root and leaf — without changing the root or the depth — now + propagate to leaves correctly. Previously a leaf could continue + routing against a stale internal path until the parent or depth + also changed. ## [0.2.0] - 2026-03-22 From 0fcf0f6f8f8195130e8ea1086ac01eebfb2dc180 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 14:01:19 +0000 Subject: [PATCH 03/12] gateway: change dns.listen default to [::1]:5353 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway is designed for systems already serving DHCP and DNS to a LAN segment (canonically an OpenWrt AP). On those systems port 53 is already taken by the existing resolver, so the prior `[::]:53` default conflicted with the gateway's intended deployment target out of the box. The OpenWrt ipk previously overrode this in its packaged config as a workaround; matching the source default to what the canonical deployment actually wants makes the override redundant and removes a foot-gun for fresh manual Linux-host installs. The redundant `dns.listen` line in `packaging/openwrt-ipk/files/etc/fips/fips.yaml` is dropped along with this change. Operators on a host without a pre-existing resolver on port 53 can opt back into the wildcard bind by setting `dns.listen: "[::]:53"` explicitly. The new default binds IPv6 loopback only — Linux IPv6 sockets bound to explicit `::1` do not accept v4-mapped traffic, so forwarders that reach the gateway over IPv4 loopback need to be pointed at an explicit IPv4 listen address instead. Touches the gateway config struct and its default-value test, the commented-out gateway example in the Debian common fips.yaml, the OpenWrt ipk config (override removed), the gateway reference / how-to / design / tutorial / troubleshoot docs, and a CHANGELOG entry under [Unreleased] -> Changed. --- CHANGELOG.md | 11 ++++++++ docs/design/fips-gateway.md | 15 +++++++---- docs/how-to/deploy-gateway.md | 26 +++++++++++------- docs/how-to/troubleshoot-gateway.md | 27 ++++++++----------- docs/reference/configuration.md | 4 +-- docs/tutorials/deploy-fips-gateway.md | 11 ++++---- packaging/common/fips.yaml | 2 +- .../openwrt-ipk/files/etc/fips/fips.yaml | 7 +++-- src/config/gateway.rs | 21 ++++++++++++--- 9 files changed, 77 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa8f99..5ec6aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -302,6 +302,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 decrypt-failure thresholds, STUN parser, gateway, NAT traversal, packaging install paths) alongside CI-side hardening for the new Windows and macOS platforms. +- Gateway `dns.listen` source default changed from `[::]:53` to + `[::1]:5353` to match the canonical deployment model (a host + already serving DHCP/DNS to a LAN segment, where port 53 is + taken by the existing resolver and `.fips` queries are forwarded + to the gateway over loopback). The OpenWrt ipk previously + overrode this in its packaged config; the override is now + redundant and has been dropped. Operators on a host without a + pre-existing resolver on port 53 can opt back into the wildcard + bind by setting `dns.listen: "[::]:53"` explicitly. The new + default binds IPv6 loopback only — forwarders that reach the + gateway over IPv4 loopback need an explicit IPv4 listen address. ### Fixed diff --git a/docs/design/fips-gateway.md b/docs/design/fips-gateway.md index b514f95..bdadccd 100644 --- a/docs/design/fips-gateway.md +++ b/docs/design/fips-gateway.md @@ -130,7 +130,7 @@ There is no `fipsctl gateway` subcommand; clients (including │ │ │ ┌──────────────┐ ┌───────────┐ │ │ │ DNS proxy │ │ Virtual │ │ - │ │ ([::]:53) │─▶│ IP pool │ │ + │ │ ([::1]:5353) │─▶│ IP pool │ │ │ │ .fips only │ │ (state │ │ │ └──────┬───────┘ │ machine) │ │ │ │ └─────┬─────┘ │ @@ -182,13 +182,18 @@ involving the DNS proxy or the pool. ### DNS Resolution Flow 1. A LAN client sends a DNS query to the gateway's listener (default - `[::]:53`, configurable via `gateway.dns.listen`). + `[::1]:5353`, configurable via `gateway.dns.listen`). The default + is loopback-only on an unprivileged port: the canonical deployment + has another resolver on the host (dnsmasq, systemd-resolved, BIND) + holding port 53 and forwarding `.fips` queries to the gateway over + loopback. Operators on a host without a pre-existing resolver on + 53 can override the listen value to `"[::]:53"` to let LAN clients + query the gateway directly. 2. If the question is not for a `.fips` domain, the gateway replies `REFUSED`. The proxy is intentionally narrow — it does not resolve public DNS, and the LAN's primary resolver should hold port 53 on - the gateway host (the OpenWrt init script wires this up by binding - the gateway listener to a non-conflicting port and configuring - dnsmasq to forward `.fips` queries there). + the gateway host (the OpenWrt init script wires dnsmasq to forward + `.fips` queries to the loopback listener automatically). 3. The gateway forwards the query to the daemon resolver (`gateway.dns.upstream`, default `[::1]:5354`). The daemon must match: an IPv6 socket bound to `[::1]` does not accept v4-mapped diff --git a/docs/how-to/deploy-gateway.md b/docs/how-to/deploy-gateway.md index 75f266f..9b335df 100644 --- a/docs/how-to/deploy-gateway.md +++ b/docs/how-to/deploy-gateway.md @@ -142,7 +142,7 @@ pick a different `fdXX::/N`). The `/112` size yields 65 536 virtual IPs, which is the gateway's hard cap regardless of CIDR width. This minimum config is enough to start the gateway. The `dns.*` block -is optional and defaults to `listen: "[::]:53"` and +is optional and defaults to `listen: "[::1]:5353"` and `upstream: "[::1]:5354"`. The full block — including `dns.*`, `pool_grace_period`, `conntrack.*`, and `port_forwards[]` — is documented in @@ -195,20 +195,28 @@ Constraints: ```yaml gateway: dns: - listen: "[::]:53" + listen: "[::1]:5353" upstream: "[::1]:5354" ttl: 60 ``` Common cases: -- **No other resolver on the host:** `listen: "[::]:53"` is the - default and works. -- **systemd-resolved is on port 53:** either disable its stub - listener (`DNSStubListener=no` in - `/etc/systemd/resolved.conf`) or move the gateway to a different - port (e.g., `[::]:5353`) and put a forwarder on 53 that delegates - `.fips` to the gateway. See +- **Another resolver on the host (the canonical case):** the default + `listen: "[::1]:5353"` is loopback-only on an unprivileged port, + so it never conflicts with dnsmasq, systemd-resolved, or BIND + holding 53. Configure the existing resolver to forward `.fips` + queries to `[::1]:5353` and you are done — this is what the + OpenWrt ipk does automatically. +- **No other resolver on the host:** set `listen: "[::]:53"` + explicitly and LAN clients can query the gateway directly. +- **systemd-resolved is on port 53:** the default already side-steps + this — leave the listen address at `[::1]:5353` and configure the + stub or a small forwarder to delegate `.fips` to the gateway. If + you would rather have the gateway on 53 directly, disable the + systemd stub listener (`DNSStubListener=no` in + `/etc/systemd/resolved.conf`) and switch `listen` to `"[::]:53"`. + See [troubleshoot-gateway.md](troubleshoot-gateway.md#port-conflict-on-the-dns-listen-port). - **Bind on the LAN address only:** `listen: "192.168.1.1:53"` exposes the resolver only to LAN clients, not loopback. diff --git a/docs/how-to/troubleshoot-gateway.md b/docs/how-to/troubleshoot-gateway.md index 324735f..0293ec1 100644 --- a/docs/how-to/troubleshoot-gateway.md +++ b/docs/how-to/troubleshoot-gateway.md @@ -82,10 +82,13 @@ for the full flag list. ### Port conflict on the DNS listen port Symptom: gateway fails to start with "address already in use" on -port 53 (or whatever `gateway.dns.listen` is set to). +the configured `gateway.dns.listen` address. -Another DNS server (systemd-resolved, dnsmasq, BIND) is bound to -the port. Identify it: +The default `[::1]:5353` is loopback-only on an unprivileged port and +should not collide with any standard resolver. If you have overridden +`dns.listen` to bind port 53 (or a LAN-side address) and another DNS +server (systemd-resolved, dnsmasq, BIND) is already bound there, +identify it: ```sh sudo ss -tulnp | grep ':53' @@ -93,18 +96,10 @@ sudo ss -tulnp | grep ':53' Two options: -- **Use an alternate listen address.** Pick a non-conflicting port - and update the gateway config: - - ```yaml - gateway: - dns: - listen: "192.168.1.1:5353" - ``` - - Then either point LAN clients at the alternate port directly, or - run a forwarding stub on port 53 that delegates `.fips` queries to - the gateway. +- **Stay on the loopback default.** Drop the override and let the + gateway use `[::1]:5353`. Configure the existing resolver to + forward `.fips` queries to it (the canonical OpenWrt deployment + works this way out of the box). - **Relocate the conflicting resolver.** Move it to a different port (or disable it if not needed) and let the gateway bind 53. @@ -189,7 +184,7 @@ not running or not enabled. Check that the daemon config has **Step 2.** Verify the gateway is listening on its DNS port: ```sh -sudo ss -tulnp | grep -E ':53\b' +sudo ss -tulnp | grep -E ':(53|5353)\b' ``` If nothing is listening on the configured `dns.listen` address, the diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 1692ca3..761717d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -699,7 +699,7 @@ Non-`.fips` queries are answered with `REFUSED`. | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `gateway.dns.listen` | string | `"[::]:53"` | LAN-facing DNS listen address. Bind on the LAN-side IP (e.g., `"192.168.1.1:53"`) or on all interfaces (`"[::]:53"`) for LAN clients to query. Bind to a non-53 port if another resolver already owns 53 on the host (see [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md)). | +| `gateway.dns.listen` | string | `"[::1]:5353"` | DNS listen address. The default binds IPv6 loopback on an unprivileged port, matching the canonical deployment where another resolver on the host (dnsmasq, systemd-resolved, BIND) holds port 53 and forwards `.fips` queries to the gateway over loopback. Bind on the LAN-side IP (e.g., `"192.168.1.1:53"`) or wildcard (`"[::]:53"`) only on hosts with no other resolver on 53 and where LAN clients query the gateway directly. See [../how-to/troubleshoot-gateway.md](../how-to/troubleshoot-gateway.md). | | `gateway.dns.upstream` | string | `"[::1]:5354"` | Upstream FIPS daemon resolver. **Must match the daemon's `dns.bind_addr` and `dns.port`.** Defaults match the daemon defaults (`::1:5354`). A v4 upstream (`"127.0.0.1:5354"`) cannot reach a daemon bound on `[::1]:5354` — Linux IPv6 sockets bound to explicit `::1` do not accept v4-mapped traffic. If you change the daemon's `dns.bind_addr`, update this field accordingly. | | `gateway.dns.ttl` | u32 | `60` | TTL in seconds on AAAA responses returned to LAN clients. Smaller values let the gateway recycle pool addresses faster; larger values reduce LAN-side query traffic. | @@ -746,7 +746,7 @@ gateway: pool: "fd01::/112" lan_interface: "enp3s0" dns: - listen: "[::]:53" + listen: "[::1]:5353" upstream: "[::1]:5354" ttl: 60 pool_grace_period: 60 diff --git a/docs/tutorials/deploy-fips-gateway.md b/docs/tutorials/deploy-fips-gateway.md index 11f6aeb..e4d4ba5 100644 --- a/docs/tutorials/deploy-fips-gateway.md +++ b/docs/tutorials/deploy-fips-gateway.md @@ -132,7 +132,6 @@ gateway: pool: "fd01::/112" # virtual IP range (up to 65535 addresses) lan_interface: "br-lan" # LAN-facing interface for proxy NDP dns: - listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here) upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default) ttl: 60 # DNS TTL and mapping lifetime (seconds) pool_grace_period: 60 # seconds after last session before reclaiming @@ -147,10 +146,11 @@ Three things to notice: - `lan_interface: "br-lan"` — the OpenWrt LAN bridge. The gateway installs proxy-NDP entries on this interface so LAN clients can ARP-equivalent for pool addresses. -- `dns.listen: "[::1]:5353"` — the gateway's DNS listener is bound - to IPv6 loopback only. dnsmasq, which owns LAN port 53, forwards - `.fips` queries to it. The init script wires up that forwarding; - you don't bind to a LAN address yourself. +- No `dns.listen` line — the source default `[::1]:5353` is exactly + what OpenWrt wants. The gateway listens on IPv6 loopback only; + dnsmasq, which owns LAN port 53, forwards `.fips` queries to it. + The init script wires up that forwarding; you don't bind to a LAN + address yourself. For the full reference, see [../reference/configuration.md § Gateway](../reference/configuration.md#gateway-gateway). @@ -321,7 +321,6 @@ gateway: pool: "fd01::/112" lan_interface: "br-lan" dns: - listen: "[::1]:5353" upstream: "[::1]:5354" ttl: 60 pool_grace_period: 60 diff --git a/packaging/common/fips.yaml b/packaging/common/fips.yaml index 0f730ef..ba3de65 100644 --- a/packaging/common/fips.yaml +++ b/packaging/common/fips.yaml @@ -94,7 +94,7 @@ transports: # pool: "fd01::/112" # lan_interface: "eth0" # dns: -# listen: "[::]:53" +# listen: "[::1]:5353" # # upstream must match the daemon's dns.bind_addr above. The # # default "[::1]:5354" matches the daemon's default. If you set # # the daemon to bind on a wildcard ("::") or specific address, diff --git a/packaging/openwrt-ipk/files/etc/fips/fips.yaml b/packaging/openwrt-ipk/files/etc/fips/fips.yaml index 7d6f2d3..764785e 100644 --- a/packaging/openwrt-ipk/files/etc/fips/fips.yaml +++ b/packaging/openwrt-ipk/files/etc/fips/fips.yaml @@ -76,16 +76,15 @@ peers: [] # Allows unmodified LAN hosts to reach FIPS mesh destinations via # DNS-allocated virtual IPs and kernel nftables NAT. # -# The gateway DNS listens on port 5353 by default so it does not conflict -# with dnsmasq on port 53. The init script configures dnsmasq to forward -# .fips queries to the gateway automatically. +# The gateway DNS listens on `[::1]:5353` by default (set in source) so it +# does not conflict with dnsmasq on port 53. The init script configures +# dnsmasq to forward .fips queries to the gateway automatically. gateway: enabled: true pool: "fd01::/112" # virtual IP range (up to 65535 addresses) lan_interface: "br-lan" # LAN-facing interface for proxy NDP dns: - listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here) upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default) ttl: 60 # DNS TTL and mapping lifetime (seconds) pool_grace_period: 60 # seconds after last session before reclaiming diff --git a/src/config/gateway.rs b/src/config/gateway.rs index 4d1248f..d524176 100644 --- a/src/config/gateway.rs +++ b/src/config/gateway.rs @@ -8,7 +8,20 @@ use std::net::SocketAddrV6; use serde::{Deserialize, Serialize}; /// Default gateway DNS listen address. -const DEFAULT_DNS_LISTEN: &str = "[::]:53"; +/// +/// Loopback-only on the unprivileged port 5353. The canonical +/// gateway deployment is a host already serving DHCP/DNS to a LAN +/// segment (e.g., an OpenWrt AP), where port 53 is taken by the +/// existing resolver and `.fips` queries are forwarded to the +/// gateway over loopback. Operators on a host without a pre-existing +/// resolver on 53 can opt back into the wildcard bind by setting +/// `dns.listen: "[::]:53"` explicitly. +/// +/// `[::1]` is IPv6 loopback only; Linux IPv6 sockets bound to +/// explicit `::1` do not accept v4-mapped traffic. Forwarders that +/// reach the gateway over IPv4 loopback (`127.0.0.1`) need to be +/// pointed at an explicit IPv4 listen address instead. +const DEFAULT_DNS_LISTEN: &str = "[::1]:5353"; /// Default upstream DNS resolver (FIPS daemon). /// @@ -118,7 +131,7 @@ pub struct PortForward { /// Gateway DNS resolver configuration (`gateway.dns.*`). #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct GatewayDnsConfig { - /// Listen address and port (default: `0.0.0.0:53`). + /// Listen address and port (default: `[::1]:5353`). #[serde(default, skip_serializing_if = "Option::is_none")] pub listen: Option, @@ -133,7 +146,7 @@ pub struct GatewayDnsConfig { } impl GatewayDnsConfig { - /// Get the listen address (default: `0.0.0.0:53`). + /// Get the listen address (default: `[::1]:5353`). pub fn listen(&self) -> &str { self.listen.as_deref().unwrap_or(DEFAULT_DNS_LISTEN) } @@ -205,7 +218,7 @@ lan_interface: "eth0" assert!(!config.enabled); assert_eq!(config.pool, "fd01::/112"); assert_eq!(config.lan_interface, "eth0"); - assert_eq!(config.dns.listen(), "[::]:53"); + assert_eq!(config.dns.listen(), "[::1]:5353"); assert_eq!(config.dns.upstream(), "[::1]:5354"); assert_eq!(config.dns.ttl(), 60); assert_eq!(config.grace_period(), 60); From 7daca6bcf16b77068a61372db0bf1b3809a6d0a3 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 13:53:28 +0000 Subject: [PATCH 04/12] packaging: refresh OpenWrt ipk README Bring the packaging README into agreement with what the ipk actually installs and the CLI surface fipsctl exposes today: - Package contents table now lists /usr/bin/fips-gateway, /etc/init.d/fips-gateway, and /etc/sysctl.d/fips-gateway.conf alongside the daemon. These have been part of the install block but were missing from the README. - fipsctl examples updated to the current command form (fipsctl show peers / show links / show sessions in place of the removed shorthands), with a pointer to the canonical CLI reference. - Service management section gains a short subsection covering the optional gateway service, including the enable/start incantation and a link to the deploy-fips-gateway tutorial. --- packaging/openwrt-ipk/README.md | 38 +++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/packaging/openwrt-ipk/README.md b/packaging/openwrt-ipk/README.md index 67bda8e..61b2f9e 100644 --- a/packaging/openwrt-ipk/README.md +++ b/packaging/openwrt-ipk/README.md @@ -11,13 +11,16 @@ For ad-hoc deployment without the build system, see | Installed path | Purpose | |---|---| | `/usr/bin/fips` | Mesh daemon | -| `/usr/bin/fipsctl` | CLI control tool (`fipsctl peers`, `fipsctl links`, …) | +| `/usr/bin/fipsctl` | CLI control tool (`fipsctl show peers`, `fipsctl show links`, …) | | `/usr/bin/fipstop` | Live TUI dashboard | -| `/etc/init.d/fips` | procd service (auto-start, crash respawn) | +| `/usr/bin/fips-gateway` | Outbound LAN gateway service (not started by default) | +| `/etc/init.d/fips` | procd service for the daemon (auto-start, crash respawn) | +| `/etc/init.d/fips-gateway` | procd service for the gateway (disabled by default) | | `/etc/fips/fips.yaml` | Node configuration (edit before first start) | | `/etc/fips/firewall.sh` | Firewall helper — accepts traffic on `fips0` | | `/etc/dnsmasq.d/fips.conf` | Forwards `.fips` DNS queries to the daemon | | `/etc/sysctl.d/fips-bridge.conf` | `br_netfilter` settings for Ethernet transport | +| `/etc/sysctl.d/fips-gateway.conf` | `proxy_ndp` and IPv6 forwarding for the gateway | | `/etc/hotplug.d/net/99-fips` | Applies firewall rules when `fips0` comes up | | `/etc/uci-defaults/90-fips-setup` | First-boot kernel module and firewall setup | | `/lib/upgrade/keep.d/fips` | Preserves `/etc/fips/` across `sysupgrade` | @@ -142,17 +145,35 @@ physical interface names for your router. **Always use physical port names /etc/init.d/fips disable ``` +### Outbound LAN gateway (optional) + +The `fips-gateway` service is installed but disabled by default. It +turns the router into an outbound gateway that bridges LAN clients +onto the FIPS mesh. Enable only after configuring a `gateway:` +section in `/etc/fips/fips.yaml`: + +```bash +/etc/init.d/fips-gateway enable +/etc/init.d/fips-gateway start +``` + +See `docs/tutorials/deploy-fips-gateway.md` in the source tree for +the full walkthrough. + ## Inspection and logs ```bash -# Peer table -fipsctl peers +# Node-level status overview +fipsctl show status -# Active sessions -fipsctl sessions +# Peer table +fipsctl show peers # Transport links -fipsctl links +fipsctl show links + +# Active end-to-end sessions +fipsctl show sessions # Live TUI dashboard fipstop @@ -161,6 +182,9 @@ fipstop logread | grep fips ``` +See [`docs/reference/cli-fipsctl.md`](../../docs/reference/cli-fipsctl.md) +for the full subcommand list. + ## Upgrading Install the new `.ipk` over the existing one: From e4f37082c2cd9455fadc212553ce745be878b96d Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 14:52:33 +0000 Subject: [PATCH 05/12] docs: gateway tutorial review feedback Two small improvements to the OpenWrt gateway deploy tutorial: - Add a router-side ping step at the top of Step 4 (post-gateway-start client test). Confirms the router itself reaches the mesh before bringing the LAN segment into the diagnosis: if this fails the troubleshooting target is the daemon / mesh side; if it succeeds and the LAN-client test below fails, the target is the LAN segment (proxy_ndp, RA pool route, or DNS forwarding through dnsmasq). - Mark the inbound port-forward section heading as Optional. The outbound half is the steady-state use of a gateway and applies to every deployment; the inbound port-forward half is a per-service opt-in that many operators won't need. --- docs/tutorials/deploy-fips-gateway.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/deploy-fips-gateway.md b/docs/tutorials/deploy-fips-gateway.md index e4d4ba5..cee3040 100644 --- a/docs/tutorials/deploy-fips-gateway.md +++ b/docs/tutorials/deploy-fips-gateway.md @@ -207,8 +207,21 @@ table install, and pool initialisation. ## Step 4: Test the outbound half from a LAN client -From a phone or laptop on the AP's LAN — anything that does IPv6 and -DNS, with no FIPS software installed — try one of the public test +Before bringing a LAN client into the picture, confirm from the AP +itself that the mesh side is still healthy after the gateway start: + +```sh +ping6 -c 2 test-us01.fips +``` + +This isolates the router-to-mesh path before involving the LAN +segment. If this fails, the troubleshooting target is the daemon / +mesh side, not the gateway-to-client side. If it succeeds and the +LAN-client test below fails, the target is the LAN segment — +`proxy_ndp`, the RA pool route, or DNS forwarding through dnsmasq. + +Now from a phone or laptop on the AP's LAN — anything that does IPv6 +and DNS, with no FIPS software installed — try one of the public test mesh nodes: ```sh @@ -272,7 +285,7 @@ nft list table inet fips_gateway You will see DNAT, SNAT, and masquerade chains populated with one rule per active mapping. -## Step 6: Add an inbound port-forward for a LAN service +## Step 6 (Optional): Add an inbound port-forward for a LAN service The outbound half is the steady-state use of a gateway. The inbound half — exposing a LAN service to mesh peers — is a separate decision, From f32bc830346d25a9ee3262904afc6f0383e2954c Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 14:48:23 +0000 Subject: [PATCH 06/12] docs: correct Ethernet MTU framing rustdoc The Ethernet data frame format is `[type:1][length:2 LE][payload]`, so the per-link payload MTU is the interface MTU minus 3 bytes, not minus 1. The 2-byte length field is required to trim NIC minimum-frame padding before AEAD verification. The implementation in src/transport/ethernet/mod.rs already uses saturating_sub(3) correctly; only the rustdoc on the effective_mtu field and the EthernetConfig.mtu field's documentation lagged behind. No behaviour change. --- src/config/transport.rs | 6 ++++-- src/transport/ethernet/mod.rs | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/config/transport.rs b/src/config/transport.rs index 2281ee4..cb7b3a8 100644 --- a/src/config/transport.rs +++ b/src/config/transport.rs @@ -267,8 +267,10 @@ pub struct EthernetConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub ethertype: Option, - /// MTU override. Defaults to the interface's MTU minus 1 (for frame type prefix). - /// Cannot exceed the interface's actual MTU. + /// MTU override. Defaults to the interface's MTU minus 3 bytes of frame + /// header (`[type:1][length:2 LE][payload]`). The 2-byte length field is + /// required to trim NIC minimum-frame padding before AEAD verification. + /// Cannot exceed the interface's actual MTU minus 3. #[serde(default, skip_serializing_if = "Option::is_none")] pub mtu: Option, diff --git a/src/transport/ethernet/mod.rs b/src/transport/ethernet/mod.rs index 759b774..60b0dbf 100644 --- a/src/transport/ethernet/mod.rs +++ b/src/transport/ethernet/mod.rs @@ -49,7 +49,9 @@ pub struct EthernetTransport { local_mac: Option<[u8; 6]>, /// Interface name (from config). interface: String, - /// Effective MTU (interface MTU - 1 for frame type prefix). + /// Effective payload MTU: interface MTU minus 3 bytes of frame header + /// (`[type:1][length:2 LE][payload]`). The 2-byte length field is required + /// to trim NIC minimum-frame padding before AEAD verification. effective_mtu: u16, /// Discovery buffer for discovered peers. discovery_buffer: Arc, From c255e3f4a2c2250476bd1796b32f8dc0ea11478d Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 14:58:53 +0000 Subject: [PATCH 07/12] session: drop dead SessionSetup/SessionAck variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both variants of SessionMessageType were never emitted anywhere in src/, and the production from_byte dispatch sites lacked Some-arms for them — any 0x00/0x01 byte that reached either dispatcher would log "Unknown..." and drop. The matching rustdoc tables described an Offset 0 msg_type byte that the encode() path has never written; the actual wire format is the FSP common prefix [ver_phase][flags][payload_len:2 LE] with body keyed by phase nibble, as documented in docs/reference/wire-formats.md. Drop the variants, drop their from_byte/to_byte/Display arms, fix the two stale rustdoc tables to describe the real wire shape, and trim the variant-iteration unit test that enumerated them. Zero on-wire behaviour change. --- src/protocol/session.rs | 78 +++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/src/protocol/session.rs b/src/protocol/session.rs index b5efa6e..6fa9465 100644 --- a/src/protocol/session.rs +++ b/src/protocol/session.rs @@ -16,15 +16,14 @@ use std::fmt; /// encrypted with session keys via the FSP pipeline. Error signals /// (CoordsRequired, PathBroken) are plaintext messages generated by transit /// routers that cannot establish e2e sessions with the source. +/// +/// Handshake messages (SessionSetup, SessionAck, SessionMsg3) are **not** +/// identified by a message-type byte; they are dispatched by the FSP phase +/// nibble in the common prefix (0x1, 0x2, 0x3 respectively). The 0x00-0x0F +/// range is therefore unallocated in this enum. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum SessionMessageType { - // Session establishment (0x00-0x0F) - /// Session setup with coordinates (warms router caches). - SessionSetup = 0x00, - /// Session acknowledgement. - SessionAck = 0x01, - // Data and metrics (0x10-0x1F) — encrypted, inner header msg_type /// Port-multiplexed service payload: `[src_port:2 LE][dst_port:2 LE][service data...]`. /// Port 256 = IPv6 shim (compressed header). Receiver dispatches by dst_port. @@ -51,8 +50,6 @@ impl SessionMessageType { /// Try to convert from a byte. pub fn from_byte(b: u8) -> Option { match b { - 0x00 => Some(SessionMessageType::SessionSetup), - 0x01 => Some(SessionMessageType::SessionAck), 0x10 => Some(SessionMessageType::DataPacket), 0x11 => Some(SessionMessageType::SenderReport), 0x12 => Some(SessionMessageType::ReceiverReport), @@ -74,8 +71,6 @@ impl SessionMessageType { impl fmt::Display for SessionMessageType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let name = match self { - SessionMessageType::SessionSetup => "SessionSetup", - SessionMessageType::SessionAck => "SessionAck", SessionMessageType::DataPacket => "DataPacket", SessionMessageType::SenderReport => "SenderReport", SessionMessageType::ReceiverReport => "ReceiverReport", @@ -330,20 +325,29 @@ impl FspInnerFlags { /// /// Carried inside a SessionDatagram envelope which provides src_addr and /// dest_addr. The SessionSetup payload contains coordinates, session flags, -/// and the Noise IK handshake message for session establishment. +/// and the Noise XK handshake message for session establishment. +/// +/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase** +/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte. +/// The `msg_type` field in the encrypted inner header applies only to +/// established-phase (0x0) messages. /// /// ## Wire Format /// -/// | Offset | Field | Size | Description | -/// |--------|------------------|---------|-------------------------------------| -/// | 0 | msg_type | 1 byte | 0x00 | -/// | 1 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIR | -/// | 2 | src_coords_count | 2 bytes | u16 LE, number of src coord entries | -/// | 4 | src_coords | 16 × n | NodeAddr array (self → root) | -/// | ... | dest_coords_count| 2 bytes | u16 LE, number of dest coord entries| -/// | ... | dest_coords | 16 × m | NodeAddr array (dest → root) | -/// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length | -/// | ... | handshake_payload| variable| Noise IK msg1 (82 bytes typical) | +/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`, +/// where `ver_phase = 0x01` (version 0, phase MSG1) and `flags = 0` for handshake. +/// +/// **Body** (after 4-byte FSP prefix): +/// +/// | Offset | Field | Size | Description | +/// |--------|-------------------|------------|-----------------------------------------------------| +/// | 0 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL | +/// | 1 | src_coords_count | 2 bytes LE | Number of source coordinate entries | +/// | 3 | src_coords | 16 × n | Source's ancestry (NodeAddr, self → root) | +/// | ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries | +/// | ... | dest_coords | 16 × m | Destination's ancestry | +/// | ... | handshake_len | 2 bytes LE | Noise payload length | +/// | ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) | #[derive(Clone, Debug)] pub struct SessionSetup { /// Source coordinates (for return path caching). @@ -456,20 +460,27 @@ impl SessionSetup { /// dest_addr. The SessionAck payload contains both the acknowledger's and /// initiator's coordinates for route cache warming (ensuring return-path /// transit nodes can route independently of the forward path) and the Noise -/// IK handshake response. +/// XK handshake response. +/// +/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase** +/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte. /// /// ## Wire Format /// -/// | Offset | Field | Size | Description | -/// |--------|------------------|---------|-------------------------------------| -/// | 0 | msg_type | 1 byte | 0x01 | -/// | 1 | flags | 1 byte | Reserved | -/// | 2 | src_coords_count | 2 bytes | u16 LE | -/// | 4 | src_coords | 16 × n | Acknowledger's coords (for caching) | -/// | ... | dest_coords_count| 2 bytes | u16 LE | -/// | ... | dest_coords | 16 × m | Initiator's coords (for return path)| -/// | ... | handshake_len | 2 bytes | u16 LE, Noise payload length | -/// | ... | handshake_payload| variable| Noise IK msg2 (33 bytes typical) | +/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`, +/// where `ver_phase = 0x02` (version 0, phase MSG2) and `flags = 0` for handshake. +/// +/// **Body** (after 4-byte FSP prefix): +/// +/// | Offset | Field | Size | Description | +/// |--------|-------------------|------------|--------------------------------------------------------------| +/// | 0 | flags | 1 byte | Reserved | +/// | 1 | src_coords_count | 2 bytes LE | Number of acknowledger coordinate entries | +/// | 3 | src_coords | 16 × n | Acknowledger's ancestry (for cache warming) | +/// | ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries | +/// | ... | dest_coords | 16 × m | Initiator's ancestry (for return-path cache warming) | +/// | ... | handshake_len | 2 bytes LE | Noise payload length | +/// | ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) | #[derive(Clone, Debug)] pub struct SessionAck { /// Acknowledger's coordinates. @@ -1156,12 +1167,11 @@ mod tests { #[test] fn test_session_message_type_roundtrip() { let types = [ - SessionMessageType::SessionSetup, - SessionMessageType::SessionAck, SessionMessageType::DataPacket, SessionMessageType::SenderReport, SessionMessageType::ReceiverReport, SessionMessageType::PathMtuNotification, + SessionMessageType::CoordsWarmup, SessionMessageType::CoordsRequired, SessionMessageType::PathBroken, SessionMessageType::MtuExceeded, From 67e660d8133525dbdd7b148ad7158fb41a74c1e8 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 15:58:12 +0000 Subject: [PATCH 08/12] deps: bump rand 0.10.0 to 0.10.1 Closes RUSTSEC-2026-0097 (unsoundness with custom logger calling rand::rng() from the log handler). Fix is the upstream deprecation of the `log` feature; no API change for our pin. --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e3ac43..5c90aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,7 +1067,7 @@ dependencies = [ "nostr", "nostr-sdk", "portable-atomic", - "rand 0.10.0", + "rand 0.10.1", "ratatui", "rtnetlink", "rustables", @@ -2391,9 +2391,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.1", diff --git a/Cargo.toml b/Cargo.toml index 0c9a543..91f84a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ secp256k1 = { version = "0.30", features = ["rand", "global-context"] } sha2 = "0.10" hkdf = "0.12" chacha20poly1305 = "0.10" -rand = "0.10.0" +rand = "0.10.1" thiserror = "2.0" bech32 = "0.11" serde = { version = "1.0", features = ["derive"] } From 1d7d0d2522b2d30d40ba5aa096f4eb2e1daac0d8 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 16:04:27 +0000 Subject: [PATCH 09/12] deps: refresh bump-safe batch Manifest pin widening: - clap 4.5 -> 4.6 (env-var rebuild correctness, derive hygiene) - tun 0.8.5 -> 0.8.7 (Linux ioctl-type fix) Lockfile-only refreshes within existing pins: - tokio -> 1.52.3 - tracing-subscriber -> 0.3.23 - socket2 -> 0.6.3 - futures -> 0.3.32 - libc -> 0.2.186 - tempfile -> 3.27.0 - bytes (transitive) -> 1.11.1+ (clears RUSTSEC-2026-0007 BytesMut::reserve overflow) All additive; no source-level migration required. --- Cargo.lock | 606 ++++++++++++++++++++++++----------------------------- Cargo.toml | 4 +- 2 files changed, 278 insertions(+), 332 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c90aa2..8861210 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -59,15 +59,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayvec" @@ -204,7 +204,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -215,7 +215,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -269,9 +269,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "block-buffer" @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -347,9 +347,9 @@ checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "c2rust-bitfields" @@ -388,7 +388,7 @@ checksum = "3b457277798202ccd365b9c112ebee08ddd57f1033916c8b8ea52f222e5b715d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -465,7 +465,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core 0.10.1", ] [[package]] @@ -532,9 +532,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -542,9 +542,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -554,27 +554,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compact_str" @@ -692,7 +692,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "crossterm_winapi", "derive_more", "document-features", @@ -758,7 +758,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -793,7 +793,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -806,7 +806,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -817,7 +817,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -828,26 +828,26 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "dbus" -version = "0.9.9" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" dependencies = [ "futures-channel", "futures-util", "libc", "libdbus-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -904,7 +904,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -947,7 +947,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "euclid" -version = "0.22.13" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df61bf483e837f88d5c2291dcf55c67be7e676b3a51acc48db3a7b163b91ed63" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ "num-traits", ] @@ -1023,9 +1023,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filedescriptor" @@ -1040,9 +1040,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "finl_unicode" @@ -1124,9 +1124,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -1139,9 +1139,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1149,15 +1149,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1166,9 +1166,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1182,32 +1182,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1217,7 +1217,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1252,20 +1251,20 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", - "rand_core 0.10.0", + "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -1319,6 +1318,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -1481,9 +1486,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1491,12 +1496,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1522,15 +1527,15 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" dependencies = [ "darling 0.23.0", "indoc", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1547,9 +1552,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "ipnetwork" @@ -1583,25 +1588,27 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "kasuari" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", @@ -1628,15 +1635,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" dependencies = [ "pkg-config", ] @@ -1663,28 +1670,27 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.10.0", "libc", ] [[package]] name = "line-clipping" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -1715,9 +1721,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -1749,9 +1755,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memmem" @@ -1776,9 +1782,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -1807,7 +1813,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "libc", "log", "netlink-packet-core", @@ -1846,7 +1852,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", @@ -1859,13 +1865,25 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cfg_aliases", "libc", "memoffset", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1964,9 +1982,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-derive" @@ -1976,7 +1994,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1999,9 +2017,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2138,7 +2156,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2178,7 +2196,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2191,7 +2209,7 @@ dependencies = [ "phf_shared", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2205,41 +2223,35 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2248,9 +2260,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plotters" @@ -2328,7 +2340,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2348,16 +2360,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "version_check", "yansi", ] [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2369,10 +2381,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.8.5" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2396,8 +2414,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20 0.10.0", - "getrandom 0.4.1", - "rand_core 0.10.0", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -2440,9 +2458,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "ratatui" @@ -2464,7 +2482,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "compact_str", "hashbrown 0.16.1", "indoc", @@ -2516,7 +2534,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hashbrown 0.16.1", "indoc", "instability", @@ -2531,9 +2549,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2555,7 +2573,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -2583,9 +2601,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2594,9 +2612,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "ring" @@ -2637,7 +2655,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ff8788a90655f715f8236507161558a966c28a2845dc54d0f9985f9bbf389b" dependencies = [ "bindgen", - "bitflags 2.10.0", + "bitflags 2.11.1", "ipnetwork", "libc", "log", @@ -2657,7 +2675,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2677,11 +2695,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2690,9 +2708,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "ring", @@ -2704,9 +2722,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -2730,9 +2748,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" @@ -2776,7 +2794,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", "serde", ] @@ -2788,7 +2806,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", ] @@ -2803,9 +2821,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -2834,7 +2852,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -2937,20 +2955,20 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df350943049174c4ae8ced56c604e28270258faec12a6a48637a7655287c9ce0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] name = "siphasher" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -2960,12 +2978,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3014,7 +3032,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3026,7 +3044,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3048,9 +3066,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3065,17 +3083,17 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3110,7 +3128,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.10.0", + "bitflags 2.11.1", "fancy-regex", "filedescriptor", "finl_unicode", @@ -3170,7 +3188,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3181,7 +3199,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3251,9 +3269,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3267,13 +3285,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3357,7 +3375,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -3383,9 +3401,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3401,9 +3419,9 @@ dependencies = [ [[package]] name = "tun" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35f176015650e3bd849e85808d809e5b54da2ba7df983c5c3b601a2a8f1095e" +checksum = "0ebb3e56bb60c1e6650c9317997862ab05864c358add3cfaa34b855ffae583d0" dependencies = [ "bytes", "cfg-if", @@ -3412,7 +3430,7 @@ dependencies = [ "ipnet", "libc", "log", - "nix 0.30.1", + "nix 0.31.2", "thiserror 2.0.18", "tokio", "tokio-util", @@ -3441,9 +3459,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -3453,9 +3471,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3468,9 +3486,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-truncate" @@ -3550,12 +3568,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "atomic", - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -3599,11 +3617,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -3612,14 +3630,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" dependencies = [ "cfg-if", "once_cell", @@ -3630,23 +3648,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3654,22 +3668,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.121" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" dependencies = [ "unicode-ident", ] @@ -3702,7 +3716,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -3710,9 +3724,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ "js-sys", "wasm-bindgen", @@ -3857,7 +3871,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "widestring", "windows-sys 0.52.0", ] @@ -3868,7 +3882,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3877,16 +3891,7 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3904,31 +3909,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -3937,96 +3925,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winreg" version = "0.55.0" @@ -4075,6 +4015,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -4096,7 +4042,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -4112,7 +4058,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -4124,7 +4070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.1", "indexmap", "log", "serde", @@ -4185,28 +4131,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -4226,7 +4172,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", "synstructure", ] @@ -4266,7 +4212,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 91f84a1..880b69d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ serde_json = "1.0" serde_yaml = "0.9" dirs = "6.0" hex = "0.4" -clap = { version = "4.5", features = ["derive"] } +clap = { version = "4.6", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "net", "time", "process", "io-util"] } @@ -36,7 +36,7 @@ nostr = { version = "0.44", features = ["std", "nip59"] } nostr-sdk = "0.44" [target.'cfg(unix)'.dependencies] -tun = { version = "0.8.5", features = ["async"] } +tun = { version = "0.8.7", features = ["async"] } libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] From b547dd70f573d71a00e644e8e022d81abd6f36ee Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 16:08:55 +0000 Subject: [PATCH 10/12] deps: bump rtnetlink 0.20.0 to 0.21.0 Pulls netlink-packet-route 0.30.0, which adds DEVCONF_FORCE_FORWARDING to Inet6DevConf for kernel 6.17+. Closes the IFLA_INET6_CONF WARN observed on kernel-6.17 hosts during fips startup. Zero source edits: FIPS does not use the deprecated link_local_address API or the renamed StablePrivacy display path. Live-host WARN-absence verification on a kernel-6.17 host is scheduled for a separate deploy. --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8861210..4a4a9e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1809,9 +1809,9 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.28.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" dependencies = [ "bitflags 2.11.1", "libc", @@ -2632,9 +2632,9 @@ dependencies = [ [[package]] name = "rtnetlink" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +checksum = "dc19f84f710fa2f337617f9bc0400260a94224bde7bae28fd8879f3771ca5784" dependencies = [ "futures-channel", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 880b69d..9ff7717 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ tun = { version = "0.8.7", features = ["async"] } libc = "0.2" [target.'cfg(target_os = "linux")'.dependencies] -rtnetlink = "0.20.0" +rtnetlink = "0.21.0" rustables = "0.8.7" # bluer/BlueZ needs glibc — see build.rs `bluer_available` cfg gate. From 6807a3213b34ec337c1502509987738e23aa7ed2 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 16:12:33 +0000 Subject: [PATCH 11/12] deps: bump windows-service 0.7 to 0.8.1 Routine refresh; raises MSRV to 1.71 (non-issue for our 2024-edition toolchain) and updates windows-sys to 0.61. FIPS uses define_windows_service!, service_main, the Error type, and Error::Winapi - all stable across 0.7 -> 0.8. Windows CI matrix is the verification gate; no live Windows nodes. --- Cargo.lock | 6 +++--- Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4a4a9e5..e3f1448 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3867,13 +3867,13 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-service" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" dependencies = [ "bitflags 2.11.1", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9ff7717..3eea986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ bluer = { version = "0.17", features = ["bluetoothd", "l2cap"] } [target.'cfg(windows)'.dependencies] wintun = "0.5" -windows-service = "0.7" +windows-service = "0.8.1" [package.metadata.deb] maintainer = "Johnathan Corgan " From b3a1fb464f9d899abc5f24f536c0dee20a8458f0 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Fri, 8 May 2026 14:23:08 +0000 Subject: [PATCH 12/12] testing: add bloom-storm chaos scenario Six-node depth-4 mesh with an induced upstream parent flap. Asserts a trailing-window ceiling on per-node `stats.bloom.sent` and a sanity floor on parent-switch count over a ~3-4 min observation window. Guards against the regression class where a spanning-tree update that changes only an internal path edge (no root or depth delta) fails to be properly contained and instead propagates to leaves as a sustained bloom-traffic oscillation, visible only at fleet scale and only after several minutes of uptime. Adds a new chaos primitive (`link_swap`) for deterministic asymmetric link-cost flapping and a post-run assertion framework with two checks: - `bloom_send_rate.max_per_node`: trailing-window ceiling on the `show_bloom` stats counter delta. Calibrated against the post-mortem reproduction harness data (per-variant counter table against pre-fix vs post-fix binaries). - `min_parent_switches.min_total`: sanity guard against a misconfigured harness where the flap inducer fires but the topology never produces a real parent-switch event (e.g., wrong root election from a different seed). Without this, the bloom-rate assertion would trivially pass on any binary including a regressed one. The runner exits 3 on assertion failure (alongside 0 success and 2 panic-detected). Threshold derivation is documented in the scenario README; the seed pin is also documented there since smallest-NodeAddr root election is sensitive to the pubkey hash ordering. Wired into ci-local.sh's chaos pool and the GitHub CI chaos matrix. --- .github/workflows/ci.yml | 3 + testing/chaos/scenarios/bloom-storm.README.md | 145 ++++++++++++++++ testing/chaos/scenarios/bloom-storm.yaml | 142 +++++++++++++++ testing/chaos/sim/__main__.py | 2 + testing/chaos/sim/assertions.py | 163 ++++++++++++++++++ testing/chaos/sim/control.py | 21 +++ testing/chaos/sim/link_swap.py | 151 ++++++++++++++++ testing/chaos/sim/runner.py | 117 +++++++++++++ testing/chaos/sim/scenario.py | 123 +++++++++++++ testing/ci-local.sh | 2 + 10 files changed, 869 insertions(+) create mode 100644 testing/chaos/scenarios/bloom-storm.README.md create mode 100644 testing/chaos/scenarios/bloom-storm.yaml create mode 100644 testing/chaos/sim/assertions.py create mode 100644 testing/chaos/sim/link_swap.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5ebed1..7361f04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -366,6 +366,9 @@ jobs: - suite: congestion-stress type: chaos scenario: congestion-stress + - suite: bloom-storm + type: chaos + scenario: bloom-storm # ── Sidecar deployment ────────────────────────────────────────── - suite: sidecar type: sidecar diff --git a/testing/chaos/scenarios/bloom-storm.README.md b/testing/chaos/scenarios/bloom-storm.README.md new file mode 100644 index 0000000..346305e --- /dev/null +++ b/testing/chaos/scenarios/bloom-storm.README.md @@ -0,0 +1,145 @@ +# bloom-storm — bloom announce storm regression scenario + +Six-node depth-4 mesh with an induced upstream parent flap. Asserts a +trailing-window ceiling on per-node `stats.bloom.sent` to catch a +regression class where a localized spanning-tree update at an +internal mid-chain edge fails to be properly contained and instead +propagates as a bloom announce storm down to the leaves. + +## Topology + +```text + n01 (root, depth 0) + / \ + n02 n03 (parent candidates pa/pb, depth 1) + \ / + n04 (flap, depth 2 — induced parent flap) + | + n05 (leaf, depth 3) + | + n06 (tail, depth 4) +``` + +n01 is expected to win the root election under the deterministic key +derivation used by the chaos runner; the run's tree snapshots +(`tree-snapshot-warmup.json`, `tree-snapshot-final.json`) record the +actual assignment. + +## Bug class guarded against + +A spanning-tree update that changes only an internal path edge — no +root change, no depth change — must not produce a sustained bloom +announce storm at downstream nodes. The original regression +(rolled-back `0caef2a`, fixed in master `4cdf382`) had this property: +in the field, a single mid-chain ancestor swap on an upstream node +caused every downstream node in its subtree to issue a bloom +announce on every parent re-evaluation tick of the upstream node, +resulting in a ~480x mesh-wide elevation in `FilterAnnounce` traffic +and a perfect bimodal flip on `est_entries`. + +## Mechanism + +The new `link_swap` chaos primitive deterministically alternates the +netem delay on `n02-n04` (5ms vs 100ms) and `n03-n04` (100ms vs 5ms) +every 4 seconds. Combined with the FIPS overrides +(`parent_hysteresis: 0.0`, `reeval_interval_secs: 1`, +`flap_threshold: 9999`, `hold_down_secs: 0`), this forces n04 to +switch parents on every swap. The whole point of the scenario is to +*force* sustained parent flapping at n04 and assert that the bloom +layer doesn't amplify it. + +## Assertions + +```yaml +assertions: + bloom_send_rate: + window_secs: 30 + max_per_node: 30 + min_parent_switches: + min_total: 10 +``` + +`bloom_send_rate` is the load-bearing assertion: per-node delta of +`stats.bloom.sent` over the trailing 30s of the run must be at most +30. Per-node deltas and the offending node IDs are written to +`assertions.txt` and the runner exits 3 on failure. + +`min_parent_switches` is a sanity guard. It fails if the run did not +record at least 10 parent switches across all nodes, which would +mean the harness fired its link swaps but the topology never produced +a real parent-switch event (e.g., wrong root election made the flap +target's parent candidates structurally non-equivalent). Without this +guard, the bloom-rate assertion would trivially pass on any binary, +including a regressed one. + +## Threshold derivation + +The original `issues/2026-0019-repro/` reproduction harness measured +(90s flap window, ~21 induced parent switches at the mid-chain +node): + +| binary | tail bloom_sent / 90s | rate scaled to 30s | +| ----------- | --------------------: | -----------------: | +| pre-fix | 21 | ~7 | +| current fix | 0 | 0 | + +Observed on this scenario at master `db5b6b1` (180s run, 35 parent +switches, 41 link swaps), per-node `bloom_sent` deltas over the +trailing 30s: + +```text +n01=5 n02=5 n03=4 n04=12 n05=6 n06=0 +``` + +n04 (the flapping node) is the highest because it is legitimately +re-sending its filter on its own parent changes. n06 (the depth-4 +"tail") sees 0, matching the calm post-fix behavior recorded in +`issues/2026-0019-repro/RESULTS.md` for the `fix2` variant. + +In the field, the regression's mesh-wide rate scaled ~480x above +steady state. A `30 / 30s / node` ceiling sits ~2.5x above the +observed maximum on fixed master and well below the +deployment-scale storm rate, giving headroom for harness jitter +without losing the ability to fail loud on the regression class. + +If `link_swap.interval_secs` or the netem delta is changed, +recalibrate. The threshold is calibrated against the values in +`scenarios/bloom-storm.yaml` as committed and the `seed: 31` pin. + +## Limitations + +- The bloom-storm regression has not been confirmed-failing here + on a regressed binary in this harness directly; the threshold is + inferred from the values measured in the dedicated + `issues/2026-0019-repro/` post-mortem harness against + `0caef2a`. To gain that confirmation, check out `0caef2a` + (or the `backup-broadcast-gate-bloom-storm` branch if still + retained), build, copy binaries into `testing/docker/`, and rerun + this scenario; the bloom-rate assertion is expected to fail loud + with n05/n06 deltas well above 30. + +- Root-election outcome is sensitive to the seed (smallest + `NodeAddr` wins, where `NodeAddr = SHA-256(pubkey)[..16]`). The + seed value `31` is pinned for this reason. The + `min_parent_switches` assertion catches drift if the seed is + changed without re-validating the topology. + +## Running locally + +```bash +# From the source repo root, with binaries already built and copied +# into testing/docker/ (see testing/scripts/build.sh). +./testing/chaos/scripts/chaos.sh bloom-storm +``` + +Run output is in `testing/chaos/sim-results/-bloom-storm/`. +Key artifacts: + +- `analysis.txt` — log analysis (panics, errors, parent switches). +- `assertions.txt` — per-assertion pass/fail with per-node deltas. +- `tree-snapshot-warmup.json`, `tree-snapshot-final.json` — control + socket tree state at warmup end and at run end. +- `runner.log` — full orchestration log. + +Total runtime: ~3.5 minutes (25s warmup + 180s scenario + ~30s +teardown). diff --git a/testing/chaos/scenarios/bloom-storm.yaml b/testing/chaos/scenarios/bloom-storm.yaml new file mode 100644 index 0000000..b819605 --- /dev/null +++ b/testing/chaos/scenarios/bloom-storm.yaml @@ -0,0 +1,142 @@ +# Bloom-storm regression scenario. +# +# Six-node depth-4 mesh with an induced upstream parent flap. Guards +# against a regression class where a spanning-tree update that changes +# only an internal path edge (no root or depth delta) fails to +# propagate to leaves, producing a sustained bloom-traffic +# oscillation visible only at fleet scale and only after several +# minutes of uptime. See README.md alongside this file for the +# bug-class description and threshold derivation. +# +# Topology (n01 should win the root election under deterministic +# key derivation; the validation snapshot at end-of-run will confirm +# the assigned root): +# +# n01 (root, depth 0) +# / \ +# n02 n03 (parent candidates pa/pb, depth 1) +# \ / +# n04 (flap, depth 2 — induced parent flap) +# | +# n05 (leaf, depth 3 — observes mid-chain swap) +# | +# n06 (tail, depth 4 — sees the cascade) +# +# The flap is induced by alternating the netem delay on n02-n04 and +# n03-n04 every 4 seconds. Because the FIPS overrides below disable +# parent-flap dampening, n04 switches parents on each swap. The bug +# class under test is whether this localized swap at depth 2 leaks +# downstream as a bloom-announce storm at depth 3 and depth 4. +# +# Assertions (bloom_send_rate.max_per_node_per_window) are evaluated +# over the trailing 30s of the run, after sustained flapping has had +# time to expose any cascade behavior. + +scenario: + name: "bloom-storm" + # Seed pinned so deterministic key derivation makes n01 win the + # root election and orders the other NodeAddrs as n02 < n03 < + # ... < n04 (so n04 is at the bottom of the tiebreak and its + # parent candidates n02/n03 are both at depth 1). The warmup tree + # snapshot in sim-results/ is the authoritative check. + # + # Other seeds may elect a different node as root or order n04 + # ahead of n02/n03, in which case the induced flap will not + # exercise the cascade path the assertion is written to catch. + seed: 31 + duration_secs: 180 + +topology: + algorithm: explicit + num_nodes: 6 + params: + adjacency: + - [n01, n02] + - [n01, n03] + - [n02, n04] + - [n03, n04] + - [n04, n05] + - [n05, n06] + subnet: "172.20.0.0/24" + ip_start: 10 + +netem: + enabled: true + default_policy: + # Calm baseline so steady-state bloom traffic is at its floor. + delay_ms: [1, 1] + jitter_ms: [0, 0] + loss_pct: [0, 0] + +# Asymmetric link-cost flap on n04's two upstream candidates. Every +# `interval_secs`, the policies on the two listed edges are swapped. +# This drives n04 to alternate parents between n02 and n03 each +# round. The 4s cadence and 5ms-vs-100ms delta come from the +# original ISSUE-2026-0019 reproduction harness — they are +# calibrated to produce a parent switch per round under the +# zero-hysteresis FIPS overrides below. +link_swap: + enabled: true + interval_secs: 4 + policies: + fast: + delay_ms: [5, 5] + jitter_ms: [0, 0] + loss_pct: [0, 0] + slow: + delay_ms: [100, 100] + jitter_ms: [0, 0] + loss_pct: [0, 0] + # Initial assignment: n02-n04 fast, n03-n04 slow. After each + # interval the assignments are swapped. + edges: + - edge: "n02-n04" + policy: fast + - edge: "n03-n04" + policy: slow + +link_flaps: + enabled: false + +traffic: + enabled: false + +logging: + rust_log: "info" + output_dir: "./sim-results" + +# Per-node assertions evaluated at the end of the run. Each asserts +# on the delta of a control-socket counter over a trailing window. +assertions: + bloom_send_rate: + # The trailing window is the last `window_secs` of the run. + # `max_per_node` is the ceiling on stats.bloom.sent delta over + # that window, per node. The threshold is set ~5x above the + # observed steady-state ceiling on fixed master and well below + # the storm-state value on the regressed binary. + window_secs: 30 + max_per_node: 30 + min_parent_switches: + # Sanity guard: detect a misconfigured harness where the flap + # inducer fires but the topology never produces a real parent + # switch (e.g., wrong root election made n04's parent + # candidates structurally non-equivalent). Without this, the + # bloom-rate assertion would trivially pass on any binary, + # including a regressed one. + # + # Floor calibrated against expected behavior under the chosen + # seed: 41 link swaps over 180s should produce >= 10 parent + # switches at n04 alone if the topology is right. + min_total: 10 + +# FIPS overrides to suppress parent-flap dampening so the asymmetric +# link-cost flap reliably produces a parent switch per round. The +# whole point of the scenario is to *force* sustained parent +# flapping at n04 and assert that bloom layer doesn't amplify it. +fips_overrides: + node: + tree: + parent_hysteresis: 0.0 + reeval_interval_secs: 1 + flap_threshold: 9999 + hold_down_secs: 0 diff --git a/testing/chaos/sim/__main__.py b/testing/chaos/sim/__main__.py index f88bcd8..b8e2684 100644 --- a/testing/chaos/sim/__main__.py +++ b/testing/chaos/sim/__main__.py @@ -54,6 +54,8 @@ def main(): if result and result.panics: sys.exit(2) + if runner.assertions_failed: + sys.exit(3) sys.exit(0) diff --git a/testing/chaos/sim/assertions.py b/testing/chaos/sim/assertions.py new file mode 100644 index 0000000..de05141 --- /dev/null +++ b/testing/chaos/sim/assertions.py @@ -0,0 +1,163 @@ +"""Post-run scenario assertions evaluated via control-socket data. + +Assertions are declared in the scenario YAML under ``assertions:`` and +are evaluated near the end of the simulation, before teardown begins. +Each failing assertion is recorded with a clear pass/fail message; the +runner exits non-zero when any assertion fails. + +Currently supported assertions: + +- ``bloom_send_rate``: per-node trailing-window ceiling on + ``stats.bloom.sent`` delta. Calibrated for the bloom-storm + regression scenario but generally usable. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass + +from .control import snapshot_all_bloom +from .scenario import BloomSendRateAssertion, MinParentSwitchesAssertion +from .topology import SimTopology + +log = logging.getLogger(__name__) + + +@dataclass +class AssertionOutcome: + name: str + passed: bool + detail: str + + +def _bloom_sent_total(node_data: dict) -> int | None: + """Extract stats.bloom.sent from a show_bloom response.""" + stats = node_data.get("stats") or {} + sent = stats.get("sent") + if sent is None: + return None + try: + return int(sent) + except (TypeError, ValueError): + return None + + +class BloomSendRateMonitor: + """Samples per-node ``stats.bloom.sent`` to evaluate a trailing-window + ceiling assertion at end-of-run. + + Usage: + m = BloomSendRateMonitor(topology, cfg) + m.sample_window_start() # called window_secs before scenario end + ... + m.sample_end() # called at scenario end + outcome = m.evaluate() + """ + + def __init__(self, topology: SimTopology, cfg: BloomSendRateAssertion): + self.topology = topology + self.cfg = cfg + self.window_start: dict[str, int] = {} + self.window_end: dict[str, int] = {} + + def sample_window_start(self) -> None: + snap = snapshot_all_bloom(self.topology) + for nid, data in snap.items(): + v = _bloom_sent_total(data) + if v is not None: + self.window_start[nid] = v + + def sample_end(self) -> None: + snap = snapshot_all_bloom(self.topology) + for nid, data in snap.items(): + v = _bloom_sent_total(data) + if v is not None: + self.window_end[nid] = v + + def evaluate(self) -> AssertionOutcome: + max_per_node = self.cfg.max_per_node + window_secs = self.cfg.window_secs + + if not self.window_start or not self.window_end: + return AssertionOutcome( + name="bloom_send_rate", + passed=False, + detail=( + f"FAIL bloom_send_rate: failed to sample window endpoints " + f"(start={len(self.window_start)} nodes, " + f"end={len(self.window_end)} nodes)" + ), + ) + + per_node_deltas: dict[str, int] = {} + for nid, end_v in self.window_end.items(): + start_v = self.window_start.get(nid) + if start_v is None: + continue + per_node_deltas[nid] = end_v - start_v + + offenders = { + nid: d for nid, d in per_node_deltas.items() if d > max_per_node + } + max_obs = max(per_node_deltas.values()) if per_node_deltas else 0 + + if offenders: + sorted_off = sorted(offenders.items(), key=lambda kv: -kv[1]) + details = ", ".join(f"{nid}={d}" for nid, d in sorted_off) + detail = ( + f"FAIL bloom_send_rate: {len(offenders)} node(s) exceeded " + f"ceiling of {max_per_node} bloom_sent over trailing " + f"{window_secs}s — offenders: {details} " + f"(all per-node deltas: " + f"{', '.join(f'{n}={v}' for n, v in sorted(per_node_deltas.items()))})" + ) + return AssertionOutcome( + name="bloom_send_rate", + passed=False, + detail=detail, + ) + + detail = ( + f"PASS bloom_send_rate: max per-node delta {max_obs} <= " + f"ceiling {max_per_node} over trailing {window_secs}s " + f"(per-node: " + f"{', '.join(f'{n}={v}' for n, v in sorted(per_node_deltas.items()))})" + ) + return AssertionOutcome( + name="bloom_send_rate", + passed=True, + detail=detail, + ) + + +def evaluate_min_parent_switches( + cfg: MinParentSwitchesAssertion, + parent_switch_count: int, +) -> AssertionOutcome: + """Sanity guard: fail the scenario if the harness-induced flap did + not produce at least ``cfg.min_total`` parent switches across the + run. Detects misconfiguration (e.g., wrong root election) where + the bloom-rate assertion would otherwise trivially pass on any + binary including the regressed one. + """ + if parent_switch_count >= cfg.min_total: + return AssertionOutcome( + name="min_parent_switches", + passed=True, + detail=( + f"PASS min_parent_switches: {parent_switch_count} switches " + f">= floor {cfg.min_total}" + ), + ) + return AssertionOutcome( + name="min_parent_switches", + passed=False, + detail=( + f"FAIL min_parent_switches: {parent_switch_count} switches " + f"< floor {cfg.min_total} — harness did not induce sufficient " + f"parent flapping; bloom-rate assertion would be trivially " + f"true. Check tree-snapshot-warmup.json: did the expected " + f"node win the root election?" + ), + ) diff --git a/testing/chaos/sim/control.py b/testing/chaos/sim/control.py index cb2fc01..f2415dc 100644 --- a/testing/chaos/sim/control.py +++ b/testing/chaos/sim/control.py @@ -149,6 +149,27 @@ def snapshot_all_mmp(topology: SimTopology) -> dict[str, dict]: return result +def query_bloom(container: str) -> dict | None: + """Query a node's bloom filter state and stats.""" + return query_node(container, "show_bloom") + + +def snapshot_all_bloom(topology: SimTopology) -> dict[str, dict]: + """Query show_bloom on all nodes, return {node_id: bloom_data}. + + Nodes that fail to respond are omitted from the result. + """ + result = {} + for node_id in sorted(topology.nodes): + container = topology.container_name(node_id) + data = query_bloom(container) + if data is not None: + result[node_id] = data + else: + log.warning("No bloom data from %s", node_id) + return result + + def query_routing(container: str) -> dict | None: """Query a node's routing stats (includes congestion counters).""" return query_node(container, "show_routing") diff --git a/testing/chaos/sim/link_swap.py b/testing/chaos/sim/link_swap.py new file mode 100644 index 0000000..76dfeda --- /dev/null +++ b/testing/chaos/sim/link_swap.py @@ -0,0 +1,151 @@ +"""Deterministic asymmetric link-cost flapping. + +Periodically rotates a fixed assignment of netem policies across a +fixed set of edges. Unlike ``LinkManager`` (random link-down events) +or the ``netem.mutation`` block (random per-edge policy mutation), +this is a deterministic, periodic flip between named policies on +named edges — useful for forcing a downstream node to switch parents +on a fixed cadence. + +Rotation is a simple one-position cyclic shift: the policy assigned +to edges[0] moves to edges[1], edges[1] -> edges[2], ..., edges[N-1] +-> edges[0]. With two edges this degenerates to a swap, which is the +intended use for the bloom-storm scenario. +""" + +from __future__ import annotations + +import logging +import random +import time + +from .netem import NetemManager, NetemParams +from .scenario import LinkSwapConfig, NetemPolicy +from .topology import SimTopology + +log = logging.getLogger(__name__) + + +def _canonical_edge(edge_str: str) -> tuple[str, str]: + """Parse "nXX-nYY" into a canonical (a, b) tuple sorted alphabetically.""" + parts = edge_str.split("-") + if len(parts) != 2: + raise ValueError(f"link_swap edge '{edge_str}' is not 'nXX-nYY' form") + a, b = sorted(parts) + return a, b + + +class LinkSwapManager: + """Manages deterministic netem-policy rotation across a fixed edge set.""" + + def __init__( + self, + topology: SimTopology, + config: LinkSwapConfig, + netem_mgr: NetemManager, + rng: random.Random, + ): + self.topology = topology + self.config = config + self.netem_mgr = netem_mgr + self.rng = rng + + # Resolve edges and validate they exist in the topology + topo_edges = {tuple(sorted([a, b])) for a, b in topology.edges} + self._edges: list[tuple[str, str]] = [] + for entry in config.edges: + edge = _canonical_edge(entry.edge) + if edge not in topo_edges: + raise ValueError( + f"link_swap edge {entry.edge} not present in topology" + ) + self._edges.append(edge) + + # Current per-position policy name. Index i in this list is + # the policy currently applied to self._edges[i]. Each rotate() + # cyclically shifts these by one position. + self._current_policies: list[str] = [e.policy for e in config.edges] + + self.swap_count = 0 + # Last apply timestamp; the runner uses this together with + # config.interval_secs to schedule rotations. + self.last_swap_at: float | None = None + + def policies(self) -> dict[str, NetemPolicy]: + return self.config.policies + + def setup_initial(self) -> None: + """Apply the initial policy assignment to each edge. + + Called once after NetemManager.setup_initial() so the link's + per-direction tc class state already exists. The initial + assignment is taken straight from the config (no rotation). + """ + for edge, policy_name in zip(self._edges, self._current_policies): + policy = self.config.policies[policy_name] + self._apply(edge, policy) + self.last_swap_at = time.time() + log.info( + "Link swap initialized: %d edges, interval %.1fs, policies=%s", + len(self._edges), + self.config.interval_secs, + list(self.config.policies.keys()), + ) + + def maybe_swap(self, now: float) -> bool: + """If the swap interval has elapsed, rotate policies. Return whether + a swap occurred so the runner can reschedule. + """ + if self.last_swap_at is None: + self.last_swap_at = now + return False + if (now - self.last_swap_at) < self.config.interval_secs: + return False + + # Cyclic shift by one position + self._current_policies = ( + [self._current_policies[-1]] + self._current_policies[:-1] + ) + for edge, policy_name in zip(self._edges, self._current_policies): + policy = self.config.policies[policy_name] + self._apply(edge, policy) + self.swap_count += 1 + self.last_swap_at = now + log.debug( + "Link swap #%d: %s", + self.swap_count, + ", ".join( + f"{a}-{b}={p}" + for (a, b), p in zip(self._edges, self._current_policies) + ), + ) + return True + + def _apply(self, edge: tuple[str, str], policy: NetemPolicy) -> None: + """Apply a policy to both directions of an edge using NetemManager.""" + params = self._sample_policy(policy) + # Drive both directions through NetemManager's per-link state so + # the rotation persists across mutate() runs and survives node + # restarts via setup_node(). + self.netem_mgr._update_link(edge[0], edge[1], params) + + def _sample_policy(self, policy: NetemPolicy) -> NetemParams: + """Sample concrete params from a policy's ranges (deterministic + when min == max, which is the bloom-storm scenario's contract). + """ + return NetemParams( + delay_ms=int(self.rng.uniform(policy.delay_ms[0], policy.delay_ms[1])), + jitter_ms=int(self.rng.uniform(policy.jitter_ms[0], policy.jitter_ms[1])), + loss_pct=round( + self.rng.uniform(policy.loss_pct[0], policy.loss_pct[1]), 1 + ), + duplicate_pct=round( + self.rng.uniform(policy.duplicate_pct[0], policy.duplicate_pct[1]), 1 + ), + reorder_pct=round( + self.rng.uniform(policy.reorder_pct[0], policy.reorder_pct[1]), 1 + ), + corrupt_pct=round( + self.rng.uniform(policy.corrupt_pct[0], policy.corrupt_pct[1]), 1 + ), + ) diff --git a/testing/chaos/sim/runner.py b/testing/chaos/sim/runner.py index a259f9d..dc4960b 100644 --- a/testing/chaos/sim/runner.py +++ b/testing/chaos/sim/runner.py @@ -12,10 +12,12 @@ import sys import time from datetime import datetime +from .assertions import AssertionOutcome, BloomSendRateMonitor, evaluate_min_parent_switches from .compose import generate_compose from .config_gen import write_configs from .control import snapshot_all_congestion, snapshot_all_mmp, snapshot_all_trees from .docker_exec import docker_compose +from .link_swap import LinkSwapManager from .links import LinkManager from .logs import AnalysisResult, analyze_logs, collect_logs, write_sim_metadata from .netem import NetemManager @@ -46,10 +48,15 @@ class SimRunner: self.veth_mgr: VethManager | None = None self.netem_mgr: NetemManager | None = None self.link_mgr: LinkManager | None = None + self.link_swap_mgr: LinkSwapManager | None = None self.traffic_mgr: TrafficManager | None = None self.node_mgr: NodeManager | None = None self.peer_churn_mgr: PeerChurnManager | None = None + # Post-run assertion monitors (sampled near end of run). + self.bloom_rate_monitor: BloomSendRateMonitor | None = None + self.assertion_outcomes: list[AssertionOutcome] = [] + @staticmethod def _resolve_output_dir(scenario: Scenario) -> str: """Build a timestamped output directory path. @@ -190,6 +197,20 @@ class SimRunner: self.topology, s.link_flaps, self.rng, netem_mgr=self.netem_mgr ) + if s.link_swap.enabled: + if not self.netem_mgr: + raise RuntimeError( + "link_swap requires netem.enabled (depends on per-link tc state)" + ) + self.link_swap_mgr = LinkSwapManager( + self.topology, s.link_swap, self.netem_mgr, self.rng, + ) + + if s.assertions.bloom_send_rate is not None: + self.bloom_rate_monitor = BloomSendRateMonitor( + self.topology, s.assertions.bloom_send_rate, + ) + if s.traffic.enabled: self.traffic_mgr = TrafficManager( self.topology, s.traffic, self.rng, down_nodes=self._down_nodes @@ -226,6 +247,12 @@ class SimRunner: if self.peer_churn_mgr: self.peer_churn_mgr.refresh_all_npubs() + # Initial link swap policy assignment (after warmup so the netem + # tc state is fully set up and the daemons have already + # discovered each other under the calm baseline). + if self.link_swap_mgr: + self.link_swap_mgr.setup_initial() + def _handle_node_restart(self, node_id: str): """Called after a node container is restarted. @@ -260,12 +287,38 @@ class SimRunner: next_churn = self._schedule_next(start, s.node_churn.interval_secs) if self.node_mgr else float("inf") next_peer_churn = self._schedule_next(start, s.peer_churn.interval_secs) if self.peer_churn_mgr else float("inf") + # Bloom-send-rate assertion: sample at window_secs before end. + bloom_window_start_at = float("inf") + if self.bloom_rate_monitor is not None: + bloom_window_start_at = ( + start + duration - s.assertions.bloom_send_rate.window_secs + ) + bloom_window_started = False + while not self._interrupted: now = time.time() elapsed = now - start if elapsed >= duration: break + # Bloom-rate window-start sampling + if ( + self.bloom_rate_monitor is not None + and not bloom_window_started + and now >= bloom_window_start_at + ): + log.info( + "Sampling bloom-rate window start (last %ds of run)...", + s.assertions.bloom_send_rate.window_secs, + ) + self.bloom_rate_monitor.sample_window_start() + bloom_window_started = True + + # Deterministic link swap (before netem mutation so a + # mutation round can't clobber the swap mid-tick). + if self.link_swap_mgr: + self.link_swap_mgr.maybe_swap(now) + # Netem mutation if self.netem_mgr and now >= next_netem: self.netem_mgr.mutate() @@ -304,6 +357,8 @@ class SimRunner: active = self.traffic_mgr.active_count if self.traffic_mgr else 0 peer_churns = self.peer_churn_mgr.churn_count if self.peer_churn_mgr else 0 status_extra = f" peer_churns={peer_churns}" if self.peer_churn_mgr else "" + if self.link_swap_mgr: + status_extra += f" swaps={self.link_swap_mgr.swap_count}" print( f"\r [{elapsed:.0f}s/{duration}s] " f"nodes={len(self.topology.nodes)} " @@ -320,11 +375,49 @@ class SimRunner: print() # Clear status line + # Bloom-rate window-end sampling. + # Done before teardown so containers are still running. + if self.bloom_rate_monitor is not None: + if not bloom_window_started: + # Loop exited before the window-start mark (e.g., + # interrupted). Take both samples now so we still + # produce a finite outcome rather than an empty dict. + log.warning( + "Bloom-rate window did not start during run; " + "sampling both endpoints at end (delta will be 0)." + ) + self.bloom_rate_monitor.sample_window_start() + log.info("Sampling bloom-rate window end...") + self.bloom_rate_monitor.sample_end() + + def _evaluate_assertions(self) -> None: + """Evaluate post-run assertions and stash outcomes on self. + + Called from teardown while containers are still running so the + assertion outcomes (which include per-node detail) can be + written alongside the run artifacts. + """ + if self.bloom_rate_monitor is not None: + outcome = self.bloom_rate_monitor.evaluate() + self.assertion_outcomes.append(outcome) + if outcome.passed: + log.info("%s", outcome.detail) + else: + log.error("%s", outcome.detail) + + @property + def assertions_failed(self) -> bool: + return any(not o.passed for o in self.assertion_outcomes) + def _teardown(self) -> AnalysisResult | None: """Stop dynamic elements, collect logs, analyze, stop containers.""" result = None if self.topology and self.compose_file: + # Evaluate post-run assertions before doing any teardown so + # control sockets are still reachable. + self._evaluate_assertions() + # Stop traffic if self.traffic_mgr: log.info("Stopping traffic sessions...") @@ -366,6 +459,30 @@ class SimRunner: f.write(result.summary()) print(result.summary()) + # Log-derived assertions (evaluated after analyze_logs so + # parent_switches and similar are populated). + mps_cfg = self.scenario.assertions.min_parent_switches + if mps_cfg is not None: + outcome = evaluate_min_parent_switches( + mps_cfg, len(result.parent_switches) + ) + self.assertion_outcomes.append(outcome) + if outcome.passed: + log.info("%s", outcome.detail) + else: + log.error("%s", outcome.detail) + + # Write assertion outcomes + if self.assertion_outcomes: + assertions_path = os.path.join(self.output_dir, "assertions.txt") + with open(assertions_path, "w") as f: + for o in self.assertion_outcomes: + f.write(o.detail + "\n") + print("=== Assertions ===") + for o in self.assertion_outcomes: + print(o.detail) + print() + # Write metadata write_sim_metadata( self.output_dir, diff --git a/testing/chaos/sim/scenario.py b/testing/chaos/sim/scenario.py index 6374ac5..79a7228 100644 --- a/testing/chaos/sim/scenario.py +++ b/testing/chaos/sim/scenario.py @@ -150,6 +150,67 @@ class IngressConfig: burst_bytes: int = 32000 +@dataclass +class LinkSwapEdge: + """One edge in a link-swap rotation. + + Edge is a canonical "nXX-nYY" string. ``policy`` names a policy + in ``LinkSwapConfig.policies``. + """ + + edge: str = "" + policy: str = "" + + +@dataclass +class LinkSwapConfig: + """Deterministic asymmetric link-cost flapping. + + On each ``interval_secs``, the policies on every pair of edges in + ``edges`` are swapped (cyclically rotated by one position). This + differs from ``link_flaps`` (random link-down events) and + ``netem.mutation`` (random per-edge policy mutation) by being a + deterministic, periodic flip between two named netem policies on + a fixed set of edges. + + Used to drive a downstream node to repeatedly switch parents on + a fixed cadence — exercises the spanning-tree rebalance path + without the noise of a random mutation walk. + """ + + enabled: bool = False + interval_secs: float = 4.0 + policies: dict[str, NetemPolicy] = field(default_factory=dict) + edges: list[LinkSwapEdge] = field(default_factory=list) + + +@dataclass +class BloomSendRateAssertion: + """Trailing-window ceiling on per-node ``stats.bloom.sent`` delta.""" + + window_secs: int = 30 + max_per_node: int = 30 + + +@dataclass +class MinParentSwitchesAssertion: + """Sanity guard: total parent switches across the run must be at + least ``min_total``. Used to detect a misconfigured harness where + the flap inducer is firing but the topology never produces a real + parent-switch event (e.g., wrong root election). + """ + + min_total: int = 1 + + +@dataclass +class AssertionsConfig: + """Optional post-run assertions evaluated against control-socket data.""" + + bloom_send_rate: BloomSendRateAssertion | None = None + min_parent_switches: MinParentSwitchesAssertion | None = None + + @dataclass class LoggingConfig: rust_log: str = "info" @@ -169,6 +230,8 @@ class Scenario: peer_churn: PeerChurnConfig = field(default_factory=PeerChurnConfig) bandwidth: BandwidthConfig = field(default_factory=BandwidthConfig) ingress: IngressConfig = field(default_factory=IngressConfig) + link_swap: LinkSwapConfig = field(default_factory=LinkSwapConfig) + assertions: AssertionsConfig = field(default_factory=AssertionsConfig) logging: LoggingConfig = field(default_factory=LoggingConfig) # Raw YAML dict appended to each generated FIPS node config. # Allows scenarios to override any FIPS config parameter @@ -320,6 +383,40 @@ def load_scenario(path: str) -> Scenario: s.ingress.tiers_kbps = [int(t) for t in tiers] s.ingress.burst_bytes = int(ig.get("burst_bytes", 32000)) + # Link swap section (deterministic asymmetric link-cost flapping). + ls = raw.get("link_swap", {}) + s.link_swap.enabled = ls.get("enabled", False) + if "interval_secs" in ls: + s.link_swap.interval_secs = float(ls["interval_secs"]) + if "policies" in ls: + s.link_swap.policies = { + name: _parse_netem_policy(pdata) + for name, pdata in ls["policies"].items() + } + if "edges" in ls: + for edata in ls["edges"]: + if not isinstance(edata, dict): + raise ValueError("link_swap.edges entries must be dicts") + edge = str(edata.get("edge", "")) + policy = str(edata.get("policy", "")) + if not edge or not policy: + raise ValueError("link_swap.edges entries require 'edge' and 'policy'") + s.link_swap.edges.append(LinkSwapEdge(edge=edge, policy=policy)) + + # Assertions section (post-run control-socket-based checks). + asrt = raw.get("assertions", {}) + if "bloom_send_rate" in asrt: + bsr = asrt["bloom_send_rate"] + s.assertions.bloom_send_rate = BloomSendRateAssertion( + window_secs=int(bsr.get("window_secs", 30)), + max_per_node=int(bsr.get("max_per_node", 30)), + ) + if "min_parent_switches" in asrt: + mps = asrt["min_parent_switches"] + s.assertions.min_parent_switches = MinParentSwitchesAssertion( + min_total=int(mps.get("min_total", 1)), + ) + # Logging section lg = raw.get("logging", {}) s.logging.rust_log = lg.get("rust_log", "info") @@ -435,3 +532,29 @@ def _validate(s: Scenario): raise ValueError(f"ingress.tiers_kbps: all values must be > 0, got {tier}") if s.ingress.burst_bytes <= 0: raise ValueError(f"ingress.burst_bytes must be > 0, got {s.ingress.burst_bytes}") + + # Validate link_swap + if s.link_swap.enabled: + if s.link_swap.interval_secs <= 0: + raise ValueError("link_swap.interval_secs must be > 0") + if len(s.link_swap.edges) < 2: + raise ValueError("link_swap.edges must list at least 2 edges to swap") + if not s.link_swap.policies: + raise ValueError("link_swap.policies must not be empty when link_swap.enabled") + for entry in s.link_swap.edges: + if entry.policy not in s.link_swap.policies: + raise ValueError( + f"link_swap.edges: policy '{entry.policy}' not in link_swap.policies" + ) + + # Validate assertions + if s.assertions.bloom_send_rate is not None: + bsr = s.assertions.bloom_send_rate + if bsr.window_secs < 1: + raise ValueError("assertions.bloom_send_rate.window_secs must be >= 1") + if bsr.max_per_node < 0: + raise ValueError("assertions.bloom_send_rate.max_per_node must be >= 0") + if bsr.window_secs > s.duration_secs: + raise ValueError( + "assertions.bloom_send_rate.window_secs must not exceed scenario duration" + ) diff --git a/testing/ci-local.sh b/testing/ci-local.sh index 9afb0ef..34661e5 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -23,6 +23,7 @@ # chaos-ethernet-only, chaos-tcp-mesh, chaos-bottleneck-parent, # chaos-cost-avoidance, chaos-cost-reeval, chaos-cost-stability, # chaos-depth-vs-cost, chaos-mixed-technology, chaos-congestion-stress, +# chaos-bloom-storm, # sidecar, dns-resolver, deb-install # # Opt-in (require --with-tor; depend on live Tor network): @@ -70,6 +71,7 @@ CHAOS_SUITES=( "depth-vs-cost depth-vs-cost" "mixed-technology mixed-technology" "congestion-stress congestion-stress" + "bloom-storm bloom-storm" ) GATEWAY_SUITES=(gateway) SIDECAR_SUITES=(sidecar)