Commit Graph
100 Commits
Author SHA1 Message Date
Johnathan Corgan 96c6b7dea8 Admit rekey msg1 from established peers when addr forms differ
Companion to the ethernet `accept_connections: false` rekey-deadlock
fix from earlier this release: the same dual-init failure mode shows
up over UDP when peers register by hostname, and the existing
addr_to_link-only carve-out in `should_admit_msg1` doesn't cover it.

The carve-out's first predicate keys `addr_to_link` by the literal
`TransportAddr` that `initiate_connection` inserted, which is the
hostname-form when a peer config carries a hostname (e.g.,
`core-vm.tail65015.ts.net:2121`). Inbound packets always arrive with
numeric source addrs because `udp_receive_loop` builds the
`TransportAddr` from the `SocketAddr` the kernel reports via
`recvfrom`. `TransportAddr` equality is byte-exact, so the two forms
don't match and the lookup misses. With `udp.accept_connections:
false` (or `udp.outbound_only: true`, which forces it false) the
gate then rejects the rekey msg1 from an established peer. The
dual-init tie-breaker stalls because the loser side never produces
msg2; both sides retry indefinitely and the winner side keeps
logging "Dual rekey initiation: we win, dropping their msg1" at 1Hz.

The earlier ethernet fix didn't generalize to this variant because
ethernet TransportAddrs are always numeric MAC bytes — both the
config-time form and the inbound-arrival form match identically.

Add a second predicate to `should_admit_msg1`: an active peer's
`current_addr()` matching `(transport_id, remote_addr)`.
`current_addr` is updated and refreshed from inbound encrypted-frame
source addrs (`handlers/encrypted.rs`), which are always numeric
`SocketAddr`-form, so this catches the established peer regardless
of how its `addr_to_link` key was originally inserted. The fast
`addr_to_link` check stays first; the iteration over peers is
bounded by peer count and only runs when the first predicate misses.

Regression coverage in this commit:

- Unit test `test_should_admit_msg1_admits_rekey_when_addr_form_differs`
  in `src/node/tests/handshake.rs`. Constructs the failing scenario
  in-process: `addr_to_link` populated with hostname-form key, peer's
  `current_addr` at the resolved numeric form, query with numeric form.
  Without the new predicate this fails immediately.

- New integration topology `rekey-outbound-only` plus matching
  docker-compose profile. Same 5-node mesh shape as `rekey-accept-off`
  but `inject-config` sets `udp.outbound_only: true` on node-b and
  rewrites node-b's peer-c address from the numeric docker IP to the
  docker hostname (`node-c:2121`), reproducing the production
  hostname-vs-numeric mismatch. The test asserts no sustained
  "Dual rekey initiation: we win" log lines on any node (>10 = bug)
  and the existing rekey health checks catch the connectivity loss
  the loop produces.

- `testing/ci-local.sh` and `.github/workflows/ci.yml` extended to
  run the new variant in the local sweep and the GitHub CI integration
  matrix alongside `rekey` and `rekey-accept-off`.

Verified locally: full `bash testing/ci-local.sh` sweep passes 29/29
suites (23m 12s) with the new variant green; 1084 unit tests pass.
2026-04-30 13:12:25 +00:00
Johnathan Corgan e641eb5b5f Drop the nostr-discovery cargo feature flag
Make Nostr-mediated overlay discovery unconditional, mirroring the
philosophy of PR #79's collapse of the tui/ble/gateway features in
favor of platform cfg gates. nostr / nostr-sdk are pure Rust over
WebSockets/TCP, so they build cleanly on every FIPS-supported
platform — there is no need for the parallel feature gate.

The flag was already in `default = [...]`, so no behavior change for
anyone using `cargo build` without `--no-default-features`. Operators
who explicitly disabled the feature will now find Nostr code present
in the binary; the runtime check `node.discovery.nostr.enabled` still
controls whether the runtime starts.

Cargo.toml:
- Remove the `[features]` table entirely.
- Drop `optional = true` from `nostr` and `nostr-sdk`.

Source: 27 cfg sites collapsed across 5 files —
`src/discovery.rs`, `src/discovery/nostr/mod.rs`,
`src/node/handlers/rx_loop.rs`, `src/node/lifecycle.rs`,
`src/node/mod.rs`. Two `#[cfg(not(feature = "nostr-discovery"))]`
fallback blocks (the udp:nat-without-runtime debug-log path and the
"feature not compiled in" warning at startup) were removed as dead
code; the always-on path already handles the missing-runtime case
via `nostr_discovery: Option<NostrDiscovery>`.

Packaging and tooling:
- `packaging/openwrt-ipk/Makefile`: drop a stale `--features gateway`
  flag (the `gateway` feature was already removed in PR #79; this
  was a leftover that the build path tolerated only because cargo
  ignored unknown feature names).
- `testing/scripts/build.sh`: drop `DEFAULT_CARGO_BUILD_ARGS=(--features
  nostr-discovery)`; defaults are empty.
- `packaging/common/fips.yaml`: drop the "requires the
  nostr-discovery feature" comment from the discovery section.

Bundled cleanup:
- Apply `cargo clippy --fix` against three pre-existing warnings in
  `src/discovery/nostr/runtime.rs` and `src/discovery/nostr/stun.rs`
  (collapsed `if let Some` chain; two redundant `as i32` casts).
  These were always present but masked when the feature gate was
  off; they surface now that the code is unconditionally compiled.
- `cargo fmt` settled two minor formatting drift sites in
  `src/bin/fips-gateway.rs` and `src/config/mod.rs`.

Tests: 1083 passed, 0 failed, 4 ignored. clippy clean. fmt clean.
2026-04-30 10:24:32 +00:00
Johnathan Corgan 37c2973e2f Test infrastructure overhaul: gateway robustness + full CI coverage
Single combined commit covering five interlocking pieces of test and
CI work that landed during the v0.3.0-prep cycle.

## fips-gateway robustness

- src/bin/fips-gateway.rs DNS upstream probe converted from a 3-second
  hard-fail to a bounded retry loop (5 attempts × 1s timeout, 1s sleep
  between attempts; ~10s worst case). Covers the cold-boot race where
  the daemon's TUN is up but the DNS responder at [::1]:5354 is still
  binding. Each failed attempt logs at INFO. In production the binary's
  retry is the live recovery mechanism; with retry it recovers silently
  instead of relying on Restart=on-failure (~5s blip + spurious ERROR
  per cycle).
- packaging/debian/fips-gateway.service `ExecStartPre` now waits up to
  30 seconds for the daemon's `fips0` TUN to appear before exec'ing
  the gateway binary. Eliminates the cold-boot race where the gateway
  exits with `fips0 interface not found` and recovers via
  `Restart=on-failure`, producing a 5-second blip and a spurious error
  log per restart cycle.
- testing/docker/entrypoint.sh gateway-mode waits up to 30s for the
  daemon's DNS responder to bind [::1]:5354 (probes once per second
  with `dig @::1 -p 5354 ... test.fips`) before exec'ing fips-gateway.
  Belt-and-suspenders with the binary's own retry: in CI we want
  deterministic startup ordering. On timeout, fall through so the
  binary's probe reports the definitive error.

## Test infrastructure DNS bind migration to ::1

After session 359's daemon DNS-bind default flipped from `127.0.0.1`
to `::1` (the production fix for ISSUE-2026-0002), the static-test
infrastructure was carrying a stale workaround that overrode the
default back to IPv4 loopback. The fips-gateway integration test
exposed the divergence: the gateway probes its DNS upstream at
`[::1]:5354` (production default) while the daemon was binding
`127.0.0.1:5354` from the template override — IPv6-explicit sockets
do not accept v4-mapped traffic, so the upstream probe exhausted
retries and the gateway exited.

- Drop the explicit `bind_addr: "127.0.0.1"` line from every test
  config that emits it: testing/static/configs/node.template.yaml,
  testing/chaos/configs/node.template.yaml, the sidecar heredoc in
  testing/docker/entrypoint.sh, testing/acl-allowlist/generate-configs.sh
  (six per-node blocks), testing/nat/scripts/generate-configs.sh, and
  the four tor templates under testing/tor/. Daemon picks up its
  production `::1` default.
- Flip the dnsmasq forwarder for `.fips` in testing/docker/Dockerfile
  from `127.0.0.1#5354` to `::1#5354` so dnsmasq on the shared test
  image continues to reach the daemon. Template and Dockerfile must
  move together since most static suites resolve `<npub>.fips` via
  the test-image dnsmasq.

## rekey-accept-off integration variant + UDP unit test

- New `rekey-accept-off` topology and docker-compose profile under
  testing/static/. 2-node variant where node-b runs with
  `udp.accept_connections: false`. Pins the regression class that
  ISSUE-2026-0004 fixed (cross-connection winner's rekey msg1 was
  being filtered by the accept_connections gate, breaking rekey).
- testing/static/scripts/rekey-test.sh accepts REKEY_TOPOLOGY and
  REKEY_ACCEPT_OFF_NODES env vars; its inject-config subcommand
  applies the per-node `udp.accept_connections: false` edit, and
  the test asserts no sustained "Dual rekey initiation" log lines.
- New UDP variant of `should_admit_msg1` admit-rekey unit test in
  src/node/tests/handshake.rs.

## ci-local.sh full integration coverage

- New runner functions and dispatcher entries for `acl-allowlist`,
  `nat-cone` / `nat-symmetric` / `nat-lan`, `rekey-accept-off`,
  `dns-resolver`, `deb-install`. Each integrates with the existing
  summary tracking via `record`.
- New `--with-tor` flag (off by default) gates `tor-socks5-outbound`
  and `tor-directory-mode` runners. Tor stays opt-in because both
  harnesses depend on the live Tor network and would introduce a
  flake source unrelated to the FIPS code.
- New suite arrays (`ACL_SUITES`, `NAT_SUITES`, `DNS_RESOLVER_SUITES`,
  `DEB_INSTALL_SUITES`, `TOR_SUITES`) drive both the default sweep
  and `--list` output.
- `run_suite` extended to accept the new suite names for `--only`
  invocations.

## GitHub CI matrix expansions

- `gateway` matrix entry runs testing/static/scripts/gateway-test.sh
  against the existing docker-compose `gateway` profile.
- `rekey-accept-off` matrix entry exercises the new topology with
  REKEY_ACCEPT_OFF_NODES=b.
- `deb-install` matrix (debian12 + ubuntu24 + ubuntu26) runs
  testing/deb-install/test.sh with privileged systemd containers.
  ~5-7 min cold cache, ~2 min warm per distro. Self-contained: builds
  its own .deb in a Debian 12 cargo-deb builder image; does not
  depend on the build job's pre-built artifact.
- `dns-resolver` matrix entry runs the full 13-scenario harness
  (per-distro systemd resolver-backend tests + real-fips end-to-end
  scenarios) in a single job. Pins the production DNS bind path that
  ISSUE-2026-0002 lived in. ~7-12 min warm, ~12-15 min cold.

Verified locally: full `bash testing/ci-local.sh` sweep passes,
including 5/5 deb-install distros and all 13 dns-resolver scenarios.
Tor-inclusive sweep (`--with-tor`) verified in a follow-up run.
2026-04-30 10:24:32 +00:00
Johnathan Corgan 674c7fe1ff UDP transport: outbound_only mode, accept_connections, loopback validation
Three related UDP transport changes that together close a real gap in
the v0.2.x "this transport accepts inbound" assumption:

- outbound_only (default false). When true, the transport binds a
  kernel-assigned ephemeral port (0.0.0.0:0) regardless of the
  configured bind_addr, refuses inbound handshakes (Transport trait's
  accept_connections() returns false), and is never advertised on
  Nostr regardless of advertise_on_nostr. Lets a node participate in
  the mesh as a pure client — initiate outbound links without
  exposing an inbound listener on a known port. Also closes the
  "loopback bind as outbound-only workaround" trap: a UDP socket
  bound to 127.0.0.1 pins 127.0.0.1 as the source IP on outbound
  packets, and Linux refuses to deliver such packets out an external
  interface — the daemon happily reports "transport started" while
  no flow ever reaches an external peer.

- accept_connections (default true). Mirrors the existing
  Ethernet/BLE knob. Lets operators run UDP in a "client" posture
  (initiate outbound, refuse inbound msg1 from new addresses) without
  switching transport. The Node-level handshake gate already carves
  out msg1 from peers established on the transport so rekey works
  on existing sessions.

- Startup validation: reject `transports.udp[*].bind_addr` set to a
  loopback address (127.x.x.x, ::1, localhost) when at least one peer
  has a non-loopback UDP address. Replaces the silent "peer link
  won't establish" failure mode with a clear error pointing at the
  bind misconfiguration. outbound_only is exempt (it overrides
  bind_addr to 0.0.0.0:0).

The is_punch_packet-based filter from the previous commit, the
Node-level admission gate landed earlier on master, and these new
config fields together cover the three distinct ways the v0.2.x
"this transport accepts inbound" assumption could break.

Tests: validation truth table (loopback+external rejected,
loopback+loopback ok, outbound_only exempt), is_loopback_addr_str
helper, accept_connections wiring (default, explicit-false,
outbound_only-forces-false), end-to-end ephemeral-bind in the runtime.
1082 tests pass with --features nostr-discovery.
2026-04-30 03:11:43 +00:00
Johnathan Corgan 23c6609a6e Ship fips0 nftables security baseline (Linux)
Add a default-deny nftables ruleset for the fips0 mesh interface as
a packaged operator asset, with a companion fips-firewall.service
oneshot unit for systemd hosts. Both are shipped disabled — the
baseline is an operator conffile and the unit is intentionally not
enabled in postinst. Activation is an explicit one-liner:

  sudo systemctl enable --now fips-firewall.service

This is deliberate: silently mutating host firewall state on package
install is hostile across the axes that matter (collisions with
existing operator nftables / Docker / OPNsense rulesets, surprise
behaviour for hosts that already filter elsewhere, conversion of an
explicit security decision into an invisible one). The opt-in
posture preserves operator agency.

The baseline closes a real default-exposure gap: any service on a
mesh host bound to a wildcard address (0.0.0.0 or [::]) is
reachable from every authenticated peer in the mesh by default.
Identity on the mesh is the peer's npub but identity is not
authorization, and the mesh is closer to a shared LAN than to the
public internet. With this filter loaded, the surface is closed
unless a drop-in opens it explicitly.

Baseline shape:
- Early-return for non-fips0 traffic (every other firewall left
  undisturbed)
- conntrack established/related accept (replies to outbound flows)
- ICMPv6 echo-request accept (ping6 reachability)
- include "/etc/fips/fips.d/*.nft" — operator-supplied allowances
- counter drop default

The accompanying docs/fips-security.md lays out the threat model
(npub-authenticated mesh is closer to a shared LAN than to the
public internet — identity is not authorization), the activation
workflow, drop-in extension recipes (allow inbound SSH from a
specific peer fd97:.../128, allow HTTP from one /64, etc), drop
visibility / debugging via the journal log rule and the drop
counter, coexistence with the runtime-managed `inet fips_gateway`
table, what the baseline does NOT cover (outbound, application
auth, mesh handshake ACL = PR #50 / IDEA-0047 territory), and
future cross-OS work (macOS PF baseline, OpenWrt fw4, gateway
abstraction).

Packaging:
- packaging/common/fips.nft       → /etc/fips/fips.nft (conffile)
- packaging/debian/fips-firewall.service → /lib/systemd/system/
- docs/fips-security.md           → /usr/share/doc/fips/
- postinst creates /etc/fips/fips.d/ (mode 0755) on configure
- prerm stops/disables fips-firewall.service on remove/purge

OpenWrt fw4 path and macOS PF baseline are deferred — separate
asymmetries, separate work.
2026-04-30 03:10:56 +00:00
Johnathan Corgan 3092c95d54 Filter stray punch probes on adopted UDP transports
When a UDP hole-punch succeeds in only one direction and the local
side adopts the punched socket, the remote end keeps retrying its
own punch attempt for several seconds. Those retries arrive on the
adopted socket and were forwarded to the FMP rx handler, which
parsed the first byte (0x4E from PUNCH_MAGIC's "NPTC" big-endian
encoding) as FMP protocol version 4 and emitted "Unknown FMP
version, dropping" once per probe. The probe stream contaminated
post-adoption handshake logs and added timing pressure during the
handshake window.

Add a transport-level filter in udp_receive_loop that silently
drops any datagram whose first 4 bytes match PUNCH_MAGIC or
PUNCH_ACK_MAGIC. Filter applies to all UDP transports, not just
adopted ones — the magic values cannot collide with valid FMP
frames (FMP version 4 is not assigned, and the protocol's
versioning is wire-format breaking), so universal filtering is
safe and removes any "is this an adopted socket" branching.

Move PUNCH_MAGIC / PUNCH_ACK_MAGIC and the new is_punch_packet()
helper from the `nostr-discovery`-gated submodule up to
crate::discovery (unconditionally compiled) so the UDP transport
can import them without requiring the feature. The
nostr-discovery types module re-exports the constants so the
existing traversal-side imports keep working unchanged.

Test: pushes a probe + ack + real frame through the receive loop
and asserts only the real frame is delivered to packet_tx.
2026-04-30 03:10:56 +00:00
Johnathan Corgan c8502cdb97 Auto-derive per-commit Debian Version for dev builds
Inject git date + short SHA into the Debian Version field when
Cargo.toml's crate version ends in -dev, so apt-based upgrade
detection works without operator workarounds. Form:
  <base>~dev+git<YYYYMMDD>.<sha>[.dirty]-1
e.g. 0.3.0~dev+git20260429.6def31b-1.

Each commit produces a uniquely-comparable Version, so
`apt install ./*.deb` and `ansible.builtin.apt: deb:` stop
silently no-op'ing when one dev .deb is installed on top of
another. The ~dev marker sorts pre-tagged-release so 0.3.0
supersedes any prior dev .deb. Tagged builds (Cargo.toml without
-dev) keep the clean <version>-1 form. --version override still
wins.

Note: legacy 0.3.0-dev-1 dev installs sort ABOVE the new form;
hosts upgrading from a legacy install will need `dpkg -i` once
on the next dev .deb to bypass apt's downgrade refusal.
2026-04-30 03:10:56 +00:00
Johnathan Corgan 6def31bcf6 Admit rekey msg1 from established peers regardless of accept_connections
The accept_connections gate at the top of handle_msg1 was applied
unconditionally, so rekey msg1 from a peer with whom an established
link already existed was dropped on the same path as fresh handshakes
from strangers. Combined with the dual-init tie-breaker, this
deadlocked at ~25 minutes when both sides' rekey timers fired
near-simultaneously: the smaller-NodeAddr side wins as initiator and
expects the larger side to consume its rekey msg1, but if the larger
side has accept_connections=false the gate dropped it. Both sides
retried at 1 Hz indefinitely; the affected peer fell out of MMP-active
rotation.

Extract the gate decision into Node::should_admit_msg1, which admits
unconditionally when addr_to_link already has an entry for the
(transport_id, remote_addr) pair (rekey/restart on an established
session) and otherwise consults the transport's accept_connections().
Fresh msg1 from strangers is still rejected before any Noise crypto.

Three unit tests pin the truth table: no transport (admit), accept_off
no-link (reject, behavior unchanged), accept_off with-link (admit, the
carve-out).

The fix generalizes for free to BLE, which has the same Node-level gate.
TCP and Tor were never subject to this deadlock because their accept
condition is runtime state (bind_addr.is_some() / onion_address.is_some()),
not a config flag.
2026-04-29 19:24:40 +00:00
Johnathan Corgan bf77ececad Fix DNS responder silent-drop on systemd-resolved deployments
The previous default configured systemd-resolved with `resolvectl dns
fips0 [<fips0_addr>]:5354`, intended to bypass an Ubuntu 22 systemd 249
interface-scoping bug. That target collides with the daemon's
mesh-interface filter on Linux: when an IPv6 packet's destination
belongs to a non-loopback interface, the kernel attributes the packet
to that interface in IPV6_PKTINFO (ipi6_ifindex == fips0) even though
loopback delivery is used (tcpdump shows lo). The mesh-interface filter
sees arrival_ifindex == mesh_ifindex and silently drops every query at
trace level — invisible to operators at the default debug level.

Net effect on stock deployments: every .fips query on systemd-resolved
hosts was silently dropped.

Daemon side
-----------

- Default `dns.bind_addr` changes from "::" to "::1" (IPv6 loopback
  only). The mesh-interface filter is then defanged on the default
  path because loopback isn't reachable from mesh peers. The filter
  remains in place defensively for operators who explicitly bind "::"
  to expose a mesh-reachable responder.

fips-dns-setup backend unification
----------------------------------

- New `try_global_drop_in` backend writes
  /etc/systemd/resolved.conf.d/fips.conf with DNS=[::1]:5354 and
  Domains=~fips. Inserted ahead of `try_resolvectl` in the dispatch
  chain. The standard loopback path has no interface scoping, so
  ipi6_ifindex reports lo and the filter passes.
- All other backends now target [::1]:5354 to match the daemon's
  default IPv6-loopback bind:
  - try_dns_delegate writes DNS=[::1]:5354
  - try_dnsmasq writes server=/fips/::1#5354
  - try_nm_dnsmasq writes server=/fips/::1#5354
- Fixed dns-delegate file path: was /etc/systemd/dns-delegate/, must
  be /etc/systemd/dns-delegate.d/ (with .d suffix). systemd-resolved
  silently ignored the previous path.
- fips-dns-teardown handles the new global-drop-in backend in cleanup.
- The legacy resolvectl per-link backend stays as a fallback,
  documented to require careful daemon bind_addr coordination.

fips-gateway upstream pairing
-----------------------------

- gateway.dns.upstream default changes from 127.0.0.1:5354 to
  [::1]:5354 to match the daemon's default bind. Linux IPv6 sockets
  bound to explicit ::1 do not accept v4-mapped traffic, so the old
  default would have caused the gateway's startup DNS reachability
  probe to time out and systemd to restart-loop the service.
- Operators who set a non-default daemon `dns.bind_addr` must also
  set `gateway.dns.upstream` to match — documented inline.

Documentation
-------------

- packaging/common/fips.yaml and packaging/openwrt-ipk fips.yaml
  examples updated; rationale for the bind_addr choice and the
  daemon/gateway pairing recorded inline.

Test coverage
-------------

- testing/dns-resolver/test.sh: real-fipsd end-to-end scenario added.
  Builds fipsd in a Debian 12 builder image (cached), runs the daemon
  with a real TUN in a privileged container, configures DNS via the
  setup script, and asserts `dig @127.0.0.53 AAAA <npub>.fips` returns
  AAAA. Refactored as a parameterized helper running across Debian
  12/13 and Ubuntu 22/24/26 (5 e2e scenarios). Backend-aware
  assertions: on systemd >= 258 the expected backend is dns-delegate;
  on older systemd it's global-drop-in. Strict content checks fail CI
  on any [::1]:5354 drift. fips-gateway also exercised in the
  debian12 scenario to lock the gateway-upstream pairing. Renamed all
  "fipsd" references to "fips" (project convention).

- testing/deb-install/ (new harness): builds the actual .deb via
  cargo-deb in a Debian 12 builder image (cached), installs via apt
  across each target distro, verifies maintainer scripts, conffile
  placement, binary placement, and end-to-end .fips resolution after
  start. Also exercises fips-gateway against the installed daemon to
  verify the gateway/daemon default pairing on a real .deb path.

- This is the test layer that was missing — the previous harness only
  verified config files were written, never that queries reached the
  daemon.

Verified: dns-resolver 78/78 assertions, deb-install 55/55 assertions
across all 5 distros (debian:12, debian:trixie, ubuntu:22.04,
ubuntu:24.04, ubuntu:26.04).
2026-04-29 12:50:11 +00:00
Johnathan Corgan 1e3b2c319e testing: rekey settle past drain window + stale tooling references
- testing/static/scripts/rekey-test.sh: bump REKEY_SETTLE from 5s to
  12s so post-rekey ping_all samples are taken after the 10s old-
  session drain window has closed (DRAIN_WINDOW_SECS in
  handlers/rekey.rs). The 5s sample window straddled the drain,
  occasionally catching old-session decrypts mid-cutover and producing
  spurious ping failures.

- testing/lib/log_analysis.py: discovery-succeeded match string is
  "proof verified, route cached" (the wording in handlers/discovery.rs);
  the regex looked for the older "caching route" wording, so the chaos
  analysis summary always reported `Succeeded: 0` regardless of how
  many successes the raw node logs actually showed.

- testing/scripts/build.sh: drop the second cargo invocation that
  passed `--features gateway --bin fips-gateway`. PR #79 removed the
  `gateway` cargo feature in favor of platform cfg gates, so this
  invocation now fails with `the package 'fips' does not contain this
  feature: gateway`. The first cargo build already produces
  fips-gateway as a workspace binary on every supported target.
2026-04-26 19:14:32 +00:00
Johnathan Corgan f16b837a12 Tune overly aggressive discovery rate limiting
The default 30s post-failure backoff (300s cap, doubling per
consecutive failure) was set to bound traffic from chatty apps
looking up unreachable targets, but in practice it dominates
cold-start mesh convergence: a single timed-out lookup during
initial bloom-filter propagation suppresses any retry for 30s, and
the existing reset triggers (parent change, new peer, first RTT,
reconnection) don't fire on a stable post-handshake topology. The
suppression window winds up dictating the protocol's effective
time-to-converge instead of bounding repeat traffic.

Replaces the single-lookup-with-internal-retry model
(`timeout_secs`/`retry_interval_secs`/`max_attempts`) with a
per-attempt timeout sequence in `node.discovery.attempt_timeouts_secs`,
defaulting to `[1, 2, 4, 8]`. Each attempt sends a fresh LookupRequest
with a new random request_id so successive attempts can take different
forwarding paths as the bloom and tree state evolve. The destination
is declared unreachable only after the sequence is exhausted (15s
total at the default).

Disables post-failure suppression by default (`backoff_base_secs`/
`backoff_max_secs` now `0`/`0`). The `DiscoveryBackoff` machinery
stays in tree (inert at zero base/cap); operators with chatty apps
generating repeat lookups against unreachable destinations can opt
back in.

`PendingLookup` field shape unchanged so the control-socket
`show_routing` JSON (`pending_lookups[].attempt`/`initiated_ms`/
`last_sent_ms`) keeps the same schema for fipstop and external
consumers; `last_sent_ms` now means "current-attempt start" under
the new state machine.
2026-04-26 19:12:19 +00:00
Johnathan CorganandGitHub cbc78091ab Rationalize cargo feature and platform-gate surface (#79)
Drop the `tui`, `ble`, and `gateway` cargo features and replace
them with platform cfg gates. Plain `cargo build` now produces
every subsystem appropriate for the target platform with no
feature flags required.

Motivation:
- `default = ["tui", "ble"]` broke `cargo build` on macOS and
  Windows because `ble` pulled in `bluer` (BlueZ, Linux-only).
  Every non-Linux packager needed `--no-default-features`.
- The feature flags on `ble` and `gateway` were redundant with
  their platform-gated deps (`bluer`, `rustables`). The parallel
  gating was inconsistent and error-prone.
- `tui` feature protected against a ratatui binary-size concern
  that no longer applies in 2026.

Cargo.toml:
- Remove `tui`, `ble`, `gateway` features; `default = []`.
- Promote `ratatui` to a non-optional top-level dependency.
- Move `rustables` from top-level optional into the Linux
  target block, non-optional.
- Split `bluer` into its own target block with
  `cfg(all(target_os = "linux", not(target_env = "musl")))`
  — BlueZ isn't available on musl router targets and
  `libdbus-sys` doesn't cross-compile to musl without pkg-config
  sysroot setup.
- Drop `required-features` from the `fipstop` and `fips-gateway`
  `[[bin]]` entries.

build.rs:
- Emit a `bluer_available` custom cfg when `target_os == "linux"`
  and `target_env != "musl"`, for use in place of the verbose
  full predicate in source cfg gates.

Source:
- Replace every `#[cfg(feature = "gateway")]` with
  `#[cfg(target_os = "linux")]`. Gateway code works on both
  glibc and musl Linux (rustables is fine on musl).
- Replace every `#[cfg(feature = "ble")]` with
  `#[cfg(bluer_available)]`. BLE-specific code (BluerIo module,
  bluer type conversions, BLE transport instance creation,
  resolve_ble_addr) is excluded on musl and non-Linux. Generic
  `BleAddr`, `BleIo` trait, `MockBleIo`, and `BleTransport<I>`
  still compile on all targets.
- `src/bin/fips-gateway.rs`: always compiled, but `main()` is
  gated to Linux. Non-Linux stub exits 1 with a diagnostic.
  Existing non-Linux packaging scripts don't ship it, so the
  stub binary sits unused.

Packaging and CI:
- Drop `--features` and `--no-default-features` flags from every
  packaging script and workflow. Defaults now match each
  platform's capabilities.
- AUR `fips-git` automatically aligns with stable `PKGBUILD`
  (both build with defaults).

Verified: `cargo build --release` with no flags produces all
four binaries on glibc Linux; all unit and integration tests
pass across Linux/macOS/Windows/OpenWrt (musl) in CI.
2026-04-24 13:42:30 -07:00
Johnathan Corgan be0708ac9b Bring acl test module into the test tree
src/node/tests/acl.rs was added by PR #50 but never declared in
src/node/tests/mod.rs, so none of its 4 unit tests ran. The tests
themselves still compile and pass against current master code —
the fix is a one-line mod declaration. Test count goes from 1031
to 1035.

No code under test changes. This only adds previously-dormant
coverage of the ACL enforcement call sites (outbound connect,
inbound msg1, outbound msg2).
2026-04-22 03:41:12 +00:00
Johnathan Corgan ad5ad53848 Merge branch 'maint'
# Conflicts:
#	CHANGELOG.md
2026-04-22 01:21:51 +00:00
Johnathan CorganandGitHub ed312ac6f2 Fix DNS resolution on Ubuntu 22 with systemd-resolved (#77)
On Ubuntu 22 (systemd 249), systemd-resolved applies interface-scoped
routing to per-link DNS servers. Configuring `resolvectl dns fips0
127.0.0.1:5354` caused resolved to attempt reaching 127.0.0.1 through
fips0 (a TUN with only fd00::/8 routes), silently failing. The DNS
responder never received queries. Newer systemd versions (250+) have
explicit handling for loopback servers on non-loopback interfaces.

Changes:

- DNS responder default bind_addr changed from "127.0.0.1" to "::"
  so it listens on all interfaces, including fips0. Bind logic in
  lifecycle.rs now parses bind_addr as IpAddr and constructs a
  SocketAddr, handling IPv6 literal formatting. Factored into
  Node::bind_dns_socket with explicit IPV6_V6ONLY=0 via socket2, so
  IPv4 clients on 127.0.0.1:5354 still reach the responder
  regardless of the kernel's net.ipv6.bindv6only sysctl.

- fips-dns-setup resolvectl backend now waits for fips0 to have a
  global IPv6 address, then configures resolved with
  [<fips0-addr>]:5354. That address is locally delivered by the
  kernel regardless of which interface resolved tries to route
  through. The dnsmasq and NetworkManager backends still use
  127.0.0.1 (they don't have the interface-scoping issue).

- Dropped hardcoded `bind_addr: "127.0.0.1"` from the packaged
  fips.yaml (Debian + OpenWrt). The shipped config was overriding
  the new default.

- DNS queries are only accepted from the localhost.

Verified end-to-end in a privileged Ubuntu 22.04 systemd container:
dig @127.0.0.53 AAAA <npub>.fips resolves cleanly through
systemd-resolved.

The dns-delegate backend (systemd 258+) still uses 127.0.0.1; it
has not been verified whether that backend has the same routing
issue.
2026-04-21 18:17:56 -07:00
Johnathan Corgan de8db82614 Make tree ancestry acceptance test deterministic
The test used Identity::generate() for the signing node while pinning
the fixed root to node_addr byte[0]=0x01. About 2/256 random identities
have byte[0] <= 0x01, which made the generated node_addr the path
minimum and triggered AncestryRootNotMinimum. Regenerate the identity
until its node_addr is numerically larger than the fixed parent and
root, so the test matches the preconditions it asserts.
2026-04-22 01:17:04 +00:00
Johnathan Corgan 03f6db58e8 Merge branch 'maint' 2026-04-21 19:33:19 +00:00
Johnathan Corgan db4b32110c Validate bloom filter fill ratio on FilterAnnounce ingress
A malformed FilterAnnounce whose fill ratio produces an implausibly
high false-positive rate is mostly useless for routing and, once
merged into our outgoing filter via bitwise OR, propagates the
saturated state to tree peers one hop per announce tick. A saturated
filter also made estimated_count() return f64::INFINITY, which
compute_mesh_size summed into its cached estimate.

handle_filter_announce now rejects inbound FilterAnnounce whose
derived FPR exceeds `node.bloom.max_inbound_fpr` (new config field,
default 0.05 ≈ fill 0.549 at k=5). Rejection is silent on the wire,
logs at WARN, and increments a new `bloom.fill_exceeded` counter. The
peer's prior stored filter and filter_sequence are left unchanged so
a single rejected announce does not wipe the peer's existing
contribution to aggregation.

After a successful outgoing FilterAnnounce send, a rate-limited WARN
fires if our own filter's FPR exceeds the same cap, surfacing
aggregation drift. Limited to once per 60 seconds via a new
Node.last_self_warn field.

BloomFilter::estimated_count() now takes max_fpr and returns
Option<f64>. Returns None for saturated filters (regardless of cap)
or when the filter's FPR exceeds max_fpr. Callers updated: debug
logs render None as "—", the Debug impl uses f64::INFINITY as "no
cap" and prints "saturated" instead of inf, control-socket JSON
emits null, and compute_mesh_size propagates None into the already-
Option<u64> estimated_mesh_size field.
2026-04-21 19:28:23 +00:00
Johnathan Corgan 5cdcff7386 Merge branch 'maint' into master
# Conflicts:
#	CHANGELOG.md
2026-04-15 17:08:50 +00:00
Johnathan Corgan 213c0e87c3 Implement inbound mesh port forwarding
Mesh peers can now reach a configured host:port on the gateway's LAN
via static port-forward rules on fips-gateway. Mirror of the outbound
LAN gateway (IDEA-0079 / TASK-2026-0056).

Config: new gateway.port_forwards list of { listen_port, proto,
target } entries. Targets are SocketAddrV6 — IPv4 is rejected at
parse time. Validation rejects zero listen ports and duplicate
(listen_port, proto) pairs.

NAT: NatManager gains a port_forwards field and set_port_forwards()
setter, rebuilt in the same atomic rustables batch as the address
mappings. Each forward emits a prerouting DNAT rule keyed on
(iifname fips0, nfproto ipv6, l4proto, tcp/udp dport) that rewrites
destination address and port via Nat::with_ip_register +
with_port_register. When any forwards are configured, a single
LAN-side masquerade is installed on (iifname fips0, oifname
lan_interface, nfproto ipv6) so the LAN host sees the gateway as
source and replies flow back through conntrack. This rule is
distinct from the existing oifname fips0 masquerade that serves the
outbound pool.

Binary: fips-gateway validates port_forwards at startup and calls
set_port_forwards after NatManager construction; startup failure
cleans up the nftables table before exiting.

Test: extend testing/static/scripts/gateway-test.sh with Phase 7
that runs a marker HTTP server on the LAN-side client (fd02::20:8080)
and, from the mesh peer, curls the gateway's fips0 address on port
18080 to exercise the full DNAT + LAN masquerade path. The
LAN-side HTTP server is started with 'docker exec -d' plus a
bind-ready poll on ss; 'docker exec bash -c "cmd &"' does not
keep the child alive past the exec session even with nohup.

Test-infra: ci-local.sh now builds/clippies/tests with
--features gateway, matching GitHub CI. Without this the release
fips-gateway binary silently stays stale across runs, since
cargo build --release alone does not compile the gateway bin.

Verified locally: cargo test --features gateway --lib (991 pass),
clippy + fmt clean, full testing/ci-local.sh green (21/21 suites
in 8m36s, including the new gateway Phase 7).
2026-04-15 05:04:37 +00:00
Johnathan CorganandGitHub 5abae0859e Add historical node and per-peer statistics with btop-style graphs (#64)
In-memory time-series history on the daemon: fast ring (1s × 3600)
plus slow ring (1m × 1440) per metric, covering node-level gauges
(mesh size, tree depth, peer count, active sessions), counters
(parent switches, aggregate bytes/packets in/out), loss rate, and
seven per-peer metrics keyed by NodeAddr (srtt_ms, loss_rate,
bytes_in/out, packets_in/out, ecn_ce). The slow ring is produced by
downsampling the fast ring on minute boundaries with Last / Sum /
Mean aggregation chosen per metric type.

Missing data is first-class. New peers back-fill NaN so every ring
shares a time axis with the node rings; peers absent from a tick
sample NaN (keeps alignment, shows as a visible gap); counter metrics
emit NaN on decrease (new link_stats baseline after reconnect) so
deltas aren't polluted. Peers are evicted 24h after last contact.
Downsampling is NaN-aware: mean skips NaN, all-NaN slow windows stay
NaN. Each history window always returns its full span at the chosen
density (1m / 10m / 1h / 24h), front-padded with NaN when the ring
hasn't yet accumulated enough samples, so switching between windows
feels like zooming in or out rather than clipping to whatever has
arrived. NaN serializes to JSON null via a custom serializer.

Control socket queries:
- show_stats_list enumerates registered metrics plus scope field and
  peer_retention_seconds.
- show_stats_history returns one metric's series for a given window
  and granularity; accepts optional peer (npub) for per-peer metrics.
- show_stats_all_history returns every metric in a single round trip;
  accepts optional peer to fetch all seven per-peer metrics.
- show_stats_peers enumerates tracked peers with lifecycle metadata.
- show_stats_history_all_peers returns one metric across all peers
  for grid rendering.
- show_status carries short sparkline windows for the dashboard so
  the client can render without extra fetches.

fipsctl gains `stats list`, `stats peers`, and `stats history
<metric>` with `--peer` (hostname or npub) and `--plot` for a Unicode
block sparkline. Plot header reports sample count, granularity,
window, and gap count; NaN renders as a blank cell.

fipstop dashboard grows inline sparklines (peer count, mesh size,
aggregate bytes in/out). A new Graphs tab stacks every metric as an
independent mini plot with its own autoscaled range; each plot uses
btop's braille 2×4 filled-area algorithm (25-entry lookup table
packing two samples per character, per-row gradient coloring for the
characteristic btop vertical-band look, rounded borders with embedded
titles). Three modes are cycled with `m`: Node (node-level stack),
MetricByPeer (small-multiples grid, 1 / 2 / 3 columns by terminal
width), PeerByMetric (existing stack scoped to one peer). `n` / `N`
cycles the mode-specific selector (metric or peer), a selector row
shows the current choice, and Graphs-tab refreshes re-fetch
show_stats_peers so selectors track peer churn. Up / Down scrolls
the stack, Left / Right cycles the window, `g` jumps to the tab.

Implements IDEA-0084 (TASK-2026-0062).
2026-04-14 10:24:16 +01:00
Johnathan Corgan 5029b40d49 Update README for current platform, transport, and example state
- Correct the transport matrix: UDP, TCP, and Tor work on Windows
  (previously shown as unsupported). Add an OpenWrt column with BLE
  disabled due to missing libdbus on the target.
- Mention the `.fips` DNS resolver and outbound LAN gateway in the
  Features list, and add the gateway bullet under What works today.
- Reframe the Linux DNS resolver setup: the `.deb` package now
  auto-configures the available backend; the manual resolvectl
  snippet is shown for tarball and manual installs.
- Expand the Examples section to list all three example deployments
  (Nostr relay sidecar, K8s sidecar, macOS WireGuard sidecar) rather
  than only the macOS one.
- Refresh Project Structure to include the `fips-gateway` binary,
  the full packaging list (macOS .pkg, Windows ZIP, OpenWrt ipk, AUR
  in addition to Debian and systemd tarball), and the examples
  directory.
- Mention macOS `.pkg` and Windows ZIP/service packaging in the
  packaging line of What works today.
2026-04-13 06:33:01 +00:00
Johnathan Corgan 6698c4d669 Expand CHANGELOG Unreleased for master-only changes
Adds entries for master-only work since 0.2.0 that wasn't captured
yet: Windows and macOS platform support, the outbound LAN gateway
and its packaging, the macOS WireGuard sidecar example, multi-backend
.fips DNS configuration, the node.log_level config, the Nostr UDP
hole punch protocol proposal doc, the MMP report interval retune,
the info-to-debug log demotion, the rekey msg1 gate fix, and the
fipstop ratatui try_init change.

Fixed entries only cover bugs present in 0.2.0. Fixes against
master-only code (BLE reliability work, new sidecar port mapping)
are rolled into their respective Added entries rather than listed
as Fixed, per Keep a Changelog conventions.
2026-04-13 06:20:45 +00:00
Johnathan Corgan 2a943e6695 Merge branch 'maint'
# Conflicts:
#	src/bin/fipsctl.rs
2026-04-13 06:05:36 +00:00
Johnathan Corgan 7e002a3883 Update CHANGELOG for pending 0.2.1 bug fixes
Captures three fixes landed on maint since 0.2.0 that were not yet
recorded: the bloom-filter greedy-tree fallback fix, auto-connect
reconnect on graceful disconnect (#60), and fipsctl mesh-address
rejection (#61).
2026-04-13 05:49:37 +00:00
Johnathan Corgan d16acf8cea Reject FIPS mesh addresses in fipsctl connect
When a user passes an fd00::/8 address as the endpoint for a udp,
tcp, or ethernet transport, the CLI previously echoed success while
the daemon silently failed the bind with EAFNOSUPPORT. Mesh ULAs are
destinations inside the mesh, not reachable transport endpoints.

fipsctl now validates the address up front and prints a clear error
with examples, exiting 1 before the control socket call. Other
transports (tor) are not inspected since they legitimately accept
non-IP endpoints.

Covered by inline tests for bare/bracketed/with-port ULA syntaxes,
non-ULA IPv6, IPv4, hostnames, and the transport filter.

Fixes #61.
2026-04-13 05:44:23 +00:00
Johnathan Corgan 42834b8008 Fix auto-connect reconnect on graceful peer disconnect
handle_disconnect() called remove_active_peer without scheduling a
reconnect, orphaning auto-connect peers on a clean upstream shutdown.
Mirror the pattern from the other three peer-removal paths (link-dead,
decrypt failure, peer restart) which all schedule reconnect after
removal.

Adds test_disconnect_schedules_reconnect regression test that verifies
handle_disconnect populates retry_pending for an auto-connect peer.
Visibility of handle_disconnect bumped to pub(in crate::node) for
direct unit-test access.

Fixes #60.
2026-04-13 05:34:51 +00:00
Johnathan Corgan 5208d3222a Merge branch 'maint' 2026-04-12 08:39:10 +00:00
Johnathan Corgan 0b81f15369 Add rustfmt to pinned toolchain components
CI format check was failing because the pinned toolchain
rust-toolchain.toml overrides the `dtolnay/rust-toolchain@stable +
rustfmt` action installation — rustup installs rustfmt onto the
stable channel, but when cargo fmt runs inside the repo, rustup
honors the 1.94.1 pin and does an on-demand install that pulls only
rustc/cargo/rust-std.

Declaring components in rust-toolchain.toml ensures the on-demand
install of the pinned toolchain includes rustfmt.
2026-04-12 08:33:38 +00:00
Johnathan CorganandGitHub fe205e74de Multi-backend DNS configuration for .fips domain (#58)
Replace the resolvectl-only fips-dns.service with a detection script
that configures whichever DNS resolver is available:

1. systemd dns-delegate (systemd >= 258, declarative drop-in)
2. systemd-resolved via resolvectl (most systemd distros)
3. dnsmasq (standalone)
4. NetworkManager with dnsmasq plugin
5. Warning with manual instructions if none found

Service reloads are non-fatal — config is written and the backend
is recorded even if the reload fails, preventing state file cleanup
issues under set -e.

Teardown reads the recorded backend from /run/fips/dns-backend and
reverses the configuration, or cleans up all possible backends if
the state file is missing.

Includes a Docker-based test harness (testing/dns-resolver/test.sh)
covering all five backends across Debian 12, Debian 13, Fedora,
and bare systems.

Fixes #52.
2026-04-11 18:45:17 +01:00
Johnathan Corgan 7494ed058d Merge branch 'maint' 2026-04-10 16:43:49 +00:00
Johnathan Corgan 19cb776216 Bump pinned toolchain to 1.94.1
The rustfmt component is no longer available for 1.94.0 on GitHub
Actions runners, causing the format check CI job to fail.
2026-04-10 16:43:45 +00:00
Johnathan Corgan 68dafbc72a Add testing/.gitignore for staged binaries and generated artifacts 2026-04-10 11:16:35 +00:00
Johnathan Corgan 0d4ffc61f0 Fix gateway test HTTP server port mismatch
The entrypoint HTTP server was binding port 80 but gateway-test.sh
curls port 8000. Maint already had 8000; the wrong port was introduced
during a merge to master.
2026-04-10 10:02:39 +00:00
Johnathan Corgan 5645284893 Update .git-blame-ignore-revs with master format commit 2026-04-10 08:47:20 +00:00
Johnathan Corgan e9da598f8a Apply rustfmt to master-only code 2026-04-10 08:47:02 +00:00
Johnathan Corgan 6196307f0e Merge branch 'maint'
# Conflicts:
#	src/bin/fips.rs
#	src/bin/fipstop/app.rs
#	src/config/mod.rs
#	src/config/node.rs
#	src/config/transport.rs
#	src/mmp/receiver.rs
#	src/mmp/sender.rs
#	src/node/handlers/handshake.rs
#	src/node/handlers/rekey.rs
#	src/node/lifecycle.rs
#	src/node/mod.rs
#	src/transport/ethernet/socket.rs
#	src/transport/mod.rs
#	src/upper/tun.rs
2026-04-10 08:46:54 +00:00
Johnathan Corgan 68bdcb2c75 Add cargo fmt --check to CI and local CI 2026-04-10 08:45:18 +00:00
Johnathan Corgan 48b1617497 Add .git-blame-ignore-revs for bulk reformat commit 2026-04-10 08:27:23 +00:00
Johnathan Corgan 13c0b70dc3 Add rustfmt formatting policy and reformat codebase
Add rustfmt.toml with stable defaults and apply cargo fmt to all
source files. This establishes a consistent formatting baseline
for CI enforcement.
2026-04-10 08:27:07 +00:00
Johnathan Corgan 60e5fefb1f Implement outbound LAN gateway
Add fips-gateway binary: a separate daemon that allows unmodified LAN
hosts to reach FIPS mesh destinations via DNS-allocated virtual IPs
and kernel nftables NAT.

Gateway DNS resolver: forwarding proxy on [::]:53 that intercepts
.fips queries, forwards to daemon resolver (localhost:5354), allocates
virtual IPs from pool, returns AAAA records. Always sends AAAA upstream
regardless of client query type, returns proper NODATA for non-AAAA.

Virtual IP pool: fd01::/112 pool with state machine lifecycle
(Allocated → Active → Draining → Free), TTL-based reclamation,
conntrack integration for session tracking.

NAT manager: nftables DNAT/SNAT rules via rustables netlink API,
per-mapping rule lifecycle, fips0 masquerade for LAN client source
address rewriting.

Network setup: local pool route, proxy NDP for virtual IPs on LAN
interface, IPv6 forwarding validation.

Control socket at /run/fips/gateway.sock with show_gateway and
show_mappings queries. fipstop Gateway tab with pool summary gauge
and mappings table.

Gateway config section in fips.yaml with pool CIDR, LAN interface,
DNS upstream, TTL, and grace period settings.

Design doc at docs/design/fips-gateway.md.

Integration test (testing/static/scripts/gateway-test.sh): three
containers verifying DNS resolution, end-to-end HTTP, NAT state,
TTL expiration, SERVFAIL fallback, and clean shutdown.
2026-04-09 16:53:32 +00:00
Johnathan Corgan aac96510d0 Merge branch 'maint' 2026-04-05 12:24:44 +00:00
Johnathan Corgan a859da7748 Fix bloom filter routing blocking greedy tree fallback
When bloom filter candidates existed but none were strictly closer to
the destination in tree-coordinate distance, find_next_hop returned
None without trying greedy tree routing. This caused NoRoute failures
in topologies where the tree parent was closer but not a bloom
candidate. Fall through to tree routing when no bloom candidate makes
progress.
2026-04-05 12:23:24 +00:00
Johnathan Corgan 4370441e48 Tune MMP link-layer report intervals for constrained transports
Raise the report interval floor from 100ms to 1000ms and ceiling from
2000ms to 5000ms. The old 100ms floor produced ~600 reports per 60s
parent evaluation cycle — far more than the ~10 needed for EWMA
convergence. The new floor yields ~60 reports/cycle, still well above
the convergence threshold, while reducing BLE overhead by 10×.

Add cold-start transition: first 5 SRTT samples use the 200ms floor
for fast initial convergence, then switch to the 1000ms steady-state
floor. Session-layer intervals unchanged (500ms–10000ms).
2026-04-05 10:38:59 +00:00
Johnathan Corgan 864a8bcc9e Merge branch 'maint' 2026-04-01 11:55:03 +00:00
Johnathan Corgan 15628e5b41 Improve ping test convergence by checking all nodes
Wait for all nodes to reach their expected peer counts instead of
only checking a single node. This prevents false failures on slower
CI runners where remote nodes (especially node E in mesh/chain
topologies) take longer to establish all links.
2026-04-01 11:54:58 +00:00
Johnathan Corgan adfbeb2348 Merge branch 'maint' 2026-04-01 11:39:44 +00:00
Johnathan Corgan 6633d22132 Improve ping test convergence by checking all nodes
Wait for all nodes to reach their expected peer counts instead of
only checking a single node. This prevents false failures on slower
CI runners where remote nodes (especially node E in mesh/chain
topologies) take longer to establish all links.
2026-04-01 11:39:40 +00:00
Johnathan Corgan 0382642d1e Merge branch 'maint'
Backport bug fixes and packaging workflows to maint:
- Fix OpenWrt ipk build: exclude BLE feature requiring D-Bus
- Add ip6 routing policy rule to protect fd00::/8
- Linux packaging workflow for release artifacts
- AUR publish workflow for tagged releases
2026-03-31 20:04:43 +00:00
Johnathan Corgan f6f2bea792 Update changelog with backported fixes and packaging workflows 2026-03-31 19:52:56 +00:00
Johnathan Corgan 97fc29eb82 Add ip6 routing policy rule to protect fd00::/8 from interception
Tailscale (and potentially other routing software) installs a default
IPv6 route in an auxiliary routing table with a policy rule that runs
before the main table. This silently diverts fd00::/8 FIPS traffic
away from the fips0 TUN device.

Add an ip6 rule (priority 5265) during TUN setup that directs fd00::/8
to the main routing table, ensuring the fips0 route is always used.
2026-03-31 19:39:27 +00:00
Johnathan Corgan bd08505002 Add docs/proposals/ with Nostr UDP hole punch protocol spec
Pre-implementation proposal for NAT traversal using Nostr relays
as the signaling channel and STUN for reflexive address discovery.
2026-03-27 19:26:11 +00:00
Johnathan Corgan 75466ae4e8 Add ip6 routing policy rule to protect fd00::/8 from interception
Tailscale (and potentially other routing software) installs a default
IPv6 route in an auxiliary routing table with a policy rule that runs
before the main table. This silently diverts fd00::/8 FIPS traffic
away from the fips0 TUN device.

Add an ip6 rule (priority 5265) during TUN setup that directs fd00::/8
to the main routing table, ensuring the fips0 route is always used.
2026-03-26 16:31:05 +00:00
Johnathan Corgan db9549885a BLE transport reliability: probe promotion, send fail-fast, pubkey timeout
- Promote probe connections directly into pool instead of dropping and
  reconnecting. Eliminates fragile two-phase connect pattern (probe →
  disconnect → reconnect) that caused race conditions on restart.

- send_async fails fast when no connection exists, triggering a
  background connect_async instead of blocking the event loop for up
  to 10s on inline L2CAP connect. Prevents control socket query
  timeouts and MMP processing stalls.

- Add 5-second timeout to pubkey_exchange recv. Without this, a peer
  that connects but never sends its pubkey blocks the calling task
  forever, killing the scan_probe_loop or accept_loop entirely.

- Add retry timer in scan_probe_loop for addresses that failed probe
  but won't get another DeviceAdded from BlueZ (deduplication).

- Clear BlueZ cached devices before starting scan so fresh
  advertisements trigger DeviceAdded after daemon restart.

- connect_async now performs pubkey exchange before promoting to pool,
  matching the accept_loop's expectation on inbound connections.

- BLE tests updated to pre-establish connections via connect_async
  since send_async no longer does inline connect.
2026-03-26 14:24:22 +00:00
Johnathan Corgan 8f1494853a BLE transport: recv buffer sizing, advertising interval, socket tuning, PHY logging
- Size receive buffer from negotiated recv_mtu instead of hardcoded 4096
  (prevents silent truncation if MTU exceeds buffer size)
- Set advertising interval to 400-600ms for deterministic behavior
  instead of depending on BlueZ driver defaults
- Enable power_forced_active on L2CAP sockets to prevent sniff-mode
  latency spikes during data transfer (best-effort, logged on failure)
- Log negotiated PHY and MTU at connection establishment for diagnostics
2026-03-26 12:31:26 +00:00
Johnathan Corgan cb6f263a1d Add node.log_level config, remove RUST_LOG from service files
Add log_level field to NodeConfig (case-insensitive, default: info).
The daemon now loads config before initializing tracing so the
configured level takes effect. RUST_LOG env var still overrides if
set.

Remove hardcoded Environment=RUST_LOG=info from systemd units and
OpenWrt procd init script — these prevented config-driven log levels
from working.
2026-03-25 15:34:47 +00:00
Johnathan Corgan d801fd0052 BLE continuous advertising, probe cooldown replaces burst beacon
Replace burst beacon pattern (1s on / 30s off) with continuous
advertising. The burst pattern caused L2CAP connect timeouts because
the remote side was no longer connectable when the probe fired after
jitter delay. BLE advertising overhead is negligible (~0.15% duty
cycle on advertising channels).

Replace the seen HashSet + jitter delay queue with a simple cooldown
map. After probing an address (success or failure), suppress re-probe
for 30s (configurable via probe_cooldown_secs). Connected peers are
filtered by pool membership check. This eliminates the bug where
failed probes permanently blacklisted addresses for the session
lifetime.

Remove config fields: scan_interval_secs, beacon_interval_secs,
beacon_duration_secs. Add: probe_cooldown_secs.
2026-03-25 15:25:10 +00:00
Johnathan Corgan 89352d3218 Add BLE L2CAP transport with scan-based auto-connect
BLE transport implementation using L2CAP Connection-Oriented Channels
(SeqPacket mode) via the bluer crate, behind cfg(feature = "ble").

Core transport:
- BleTransport<I> generic over BleIo trait (BluerIo prod, MockBleIo test)
- Connection pool with priority eviction (static > discovered, max 7)
- Connect-on-send via connect_inline() matching TCP behavior
- Per-connection receive loops with pool cleanup on disconnect

Discovery and probing:
- Combined scan_probe_loop using select! over scanner events and a
  BinaryHeap delay queue with per-entry random jitter (0-5s) to prevent
  herd effects when multiple nodes see the same beacon simultaneously
- Pre-handshake pubkey exchange ([0x00][pubkey:32]) for IK identity
- Cross-probe tie-breaker: smaller NodeAddr's outbound wins (same
  convention as FMP/FSP rekey dual-initiation)
- Probed peers reported to DiscoveryBuffer; pool fills through normal
  node-layer auto-connect -> send_async -> connect_inline path

Beacon management:
- Periodic advertising: 1s burst every 30s (configurable via
  beacon_interval_secs / beacon_duration_secs)
- FIPS service UUID for scan filtering

Configuration (all fields optional with defaults):
- adapter, psm, mtu, max_connections, connect_timeout_ms
- advertise, scan, auto_connect, accept_connections
- beacon_interval_secs (30), beacon_duration_secs (1)

Hardware validated with two BLE nodes:
- 2048-byte MTU, ~60-160ms RTT, zero-config auto-connect
- BLE spike tool at testing/ble/ for standalone adapter validation

42 unit tests + 4 node-level integration tests, all CI-compatible
via MockBleIo (no hardware required). tokio test-util added for
time-dependent scan/probe tests.
2026-03-25 04:21:46 +00:00
Johnathan Corgan d3385b902a Merge branch 'maint' 2026-03-25 03:08:31 +00:00
Johnathan Corgan 7f33e5f867 Fix lookup-request.svg: remove stale visited bloom filter
The visited_bits field (hash_cnt + 256 bytes) was removed from the
LookupRequest wire format in the discovery-rework (bloom-guided tree
routing replaced the visited filter). Update the diagram to match the
current 46 + 16n byte format.
2026-03-25 03:07:46 +00:00
Johnathan Corgan fc8c0dce15 Increase rekey test convergence wait from 5s to 15s
Phase 1 pre-rekey baseline was failing intermittently on CI runners
because 5s wasn't enough for multi-hop discovery (B→D requires
B→C→D). The mesh always converged by Phase 3, confirming this was
purely a timing issue.
2026-03-23 18:02:35 +00:00
Johnathan Corgan 71e2955da0 Fix rekey integration test after logging demotion (b8fbecc)
The rekey test greps container logs for initiator cutover messages that
were demoted to debug level. Override RUST_LOG in the rekey profile to
enable debug for fips::node::handlers::rekey so the test assertions
can find them.
2026-03-23 16:38:33 +00:00
Johnathan Corgan 9519dc1cf4 Merge branch 'maint' 2026-03-23 13:50:31 +00:00
Johnathan Corgan bce5619b74 Fix control socket path detection for non-group users (#30)
Check /run/fips/ directory existence instead of the socket file inside
it. Users not in the fips group can stat the directory but not traverse
it, so the socket file check silently returned false and fell back to
$XDG_RUNTIME_DIR with a misleading "No such file" error.
2026-03-23 13:49:52 +00:00
Johnathan Corgan b8fbecc575 Demote 35 info-level log messages to debug for cleaner production output
Reduce info-level noise by moving intermediate steps, periodic
telemetry, cross-connection resolution details, and redundant messages
to debug. Info output now focuses on operator-relevant state changes:
lifecycle events, peer promotions, session establishment, parent
switches, and transport start/stop.

Key categories demoted:
- Handshake cross-connection resolution mechanics (10 messages)
- Periodic MMP link/session metric reports (4 messages)
- TUN cleanup messages redundant with lifecycle shutdown (4 messages)
- Transport "packet channel closed" shutdown messages (4 messages)
- Retry scheduling, discovery lookup initiation, other intermediate steps

Change default RUST_LOG from debug to info in systemd unit files.
2026-03-23 04:11:57 +00:00
Johnathan Corgan 0ff3f029ed Merge maint into master 2026-03-23 03:23:11 +00:00
Johnathan Corgan 9757877c0a Update README and changelog for v0.2.0 release 2026-03-23 03:21:44 +00:00
Johnathan Corgan 94884876b8 Bump version to 0.2.1-dev 2026-03-23 03:21:32 +00:00
Johnathan Corgan 9d9e2b05a1 Bump version to 0.3.0-dev 2026-03-23 03:19:26 +00:00
Johnathan Corgan a16370e78d Update changelog, version, and design docs for v0.2.0
Bump version to 0.2.0 and finalize changelog with discovery rework,
Tor transport, connect/disconnect commands, reproducible builds, and
12 bug fixes.

Update design documentation for discovery protocol rework:
- fips-wire-formats.md: remove visited bloom filter from LookupRequest,
  update size calculations
- fips-mesh-operation.md: replace flooding description with bloom-guided
  tree routing, add retry/backoff/rate-limiting subsections
- fips-configuration.md: add 5 new discovery config parameters, update
  control socket description for connect/disconnect commands
2026-03-22 20:26:09 +00:00
Johnathan Corgan 537eaf0db7 Fix tarball reproducibility: default SOURCE_DATE_EPOCH and sort entries
build-tarball.sh only applied --mtime when SOURCE_DATE_EPOCH was
explicitly set, and did not sort tar entries. This produced different
tarballs across builds due to varying timestamps and filesystem
ordering. Default SOURCE_DATE_EPOCH to the git commit timestamp and
add --sort=name for deterministic output.
2026-03-22 20:26:00 +00:00
Johnathan Corgan 6aff490251 Improve discovery protocol: bloom-guided tree routing with fallback
Replace discovery flooding with bloom-filter-guided tree routing:
lookups sent only to tree peers (parent + children) whose bloom
filter contains the target. If no tree peer matches, fall back to
non-tree peers with bloom matches before dropping the request. This
produces single-path forwarding through the spanning tree (90%
traffic reduction) while recovering from dead ends caused by stale
bloom filters, tree restructuring, or transit node failures.

Remove visited bloom filter from LookupRequest wire format (-257
bytes per request). Tree routing is inherently loop-free; request_id
dedup handles edge cases during tree restructuring. Add response-
forwarded flag to prevent response routing loops from convergent
request paths.

Add originator-side exponential backoff (30s base, 300s cap) after
lookup timeouts and bloom misses. Backoff resets on topology changes
(parent switch, new peer, first RTT, reconnection).

Add transit-side per-target rate limiting (2s minimum interval) for
forwarded lookups as defense-in-depth.

Add discovery retry within the timeout window (default: send at T=0,
retry at T=5s, fail at T=10s) to compensate for single-path fragility.
Lookups with zero eligible tree peers fail immediately.

Improve discovery logging: promote key events to info (initiation,
success, timeout with failure count). Add debug logging for dedup,
pending packet retry, backoff suppression, forward rate limiting,
and backoff reset.

New config: discovery.backoff_base_secs, backoff_max_secs,
forward_min_interval_secs, retry_interval_secs, max_attempts.
New stats: req_backoff_suppressed, req_forward_rate_limited,
req_bloom_miss, req_no_tree_peer, req_fallback_forwarded.
Removed: req_already_visited, visited bloom filter.
2026-03-22 20:02:00 +00:00
Johnathan Corgan fed6cc6987 Fix chaos sim DNS pipeline, ephemeral npub tracking, and analysis
Three infrastructure bugs fixed:

- DNS resolution never populated the identity cache because Docker
  overwrites /etc/resolv.conf at container start. Fix: bind-mount
  resolv.conf in the chaos compose template, matching the static and
  sidecar test configs.

- Traffic generator used config-time npubs for iperf3 targets, but
  ephemeral nodes generate fresh keypairs at startup. Fix: share the
  peer churn manager's runtime npub cache with the traffic manager.

- Log analysis had no discovery counters and used wrong patterns.
  Fix: add discovery section with correct log message matching.

Also adds timestamped output directories (YYYYMMDD-HHMMSS-scenario),
coord_ttl_secs override in maelstrom.yaml, FIPS_SIM_OUTPUT env var
support, and maelstrom-sparse scenario for sparse topology testing.
2026-03-20 16:14:10 +00:00
Johnathan Corgan 5053cf673d Add connect/disconnect control commands and maelstrom chaos scenario
Add runtime peer management to the FIPS daemon via control socket
commands, and a new chaos simulation scenario that exercises dynamic
topology mutation with ephemeral node identities.

Daemon (connect/disconnect commands):
- Extend control socket Request with optional params field
- Add commands.rs module for mutating command dispatch, separate from
  read-only queries
- Add api_connect() on Node: builds ephemeral PeerConfig (no auto-
  reconnect), pre-seeds identity cache, reuses initiate_peer_connection
- Add api_disconnect() on Node: calls remove_active_peer(), clears
  retry_pending to suppress reconnection
- Route non-show_* commands to async command dispatch in rx_loop

fipsctl CLI:
- Add Connect and Disconnect subcommands accepting npub or hostname
- Resolve hostnames from /etc/fips/hosts before sending to daemon
- Refactor socket I/O into reusable send_request helper

Chaos simulator (maelstrom scenario):
- Add PeerChurnManager: periodically disconnects a random active link
  and connects a random unconnected node pair via control socket
- Add send_command() to control.py using base64-encoded JSON payloads
  to avoid shell quoting issues in docker exec
- Add PeerChurnConfig to scenario with interval and ephemeral_fraction
- Ephemeral identity support: nodes configured without nsec generate
  fresh keypairs on restart; simulator queries show_status for new
  npub and updates its cache via on_node_restart callback
- Add maelstrom.yaml: all chaos dimensions (netem, link flaps, node
  churn, peer topology churn, traffic) with 50% ephemeral identity
2026-03-20 05:44:31 +00:00
Johnathan Corgan 8d51dbd268 Move K8s sidecar from testing/ to examples/
The K8s sidecar is a standalone example, not a test harness — it has
no CI integration unlike testing/sidecar/. Move it to examples/
alongside sidecar-nostr-relay/ where it belongs. Update internal path
references in Dockerfile, build.sh, and README.
2026-03-19 19:36:39 +00:00
Johnathan Corgan 9e63b42bd9 Consolidate Docker test harness infrastructure
Replace 4 near-identical per-harness Docker setups with unified shared
infrastructure. Net result: -463 lines across 55 files, faster CI
(8.5 min vs ~13.5 min), 14 scenarios (down from 21).

Unified Docker image (testing/docker/):
- Single Dockerfile (trixie-slim) with FIPS_TEST_MODE env var for
  mode dispatch: default, chaos, sidecar, tor-socks5, tor-directory
- Single entrypoint.sh with conditional logic per mode
- Replaces 5 Dockerfiles, 3 entrypoints, 4 resolv.conf copies

Shared build and libraries (testing/scripts/, testing/lib/):
- testing/scripts/build.sh: single build script with macOS zigbuild
  support, replaces 4 per-harness copies
- testing/lib/derive_keys.py: shared key derivation module, replaces
  3 copies of derive-keys.py
- testing/lib/log_analysis.py: shared log analysis extracted from
  chaos sim/logs.py, with CLI interface and rekey cutover tracking
- testing/lib/wait-converge.sh: shared convergence wait helpers
  (wait_for_links, wait_for_peers) using fipsctl JSON polling

Scenario consolidation:
- Remove 7 redundant scenarios: tcp-chain (subsumed by tcp-mesh),
  tcp-only (subsumed by tcp-mesh), chaos-10 (replaced by
  churn-mixed --nodes 10), churn-10/churn-20/churn-20-mixed
  (subsumed by parameterized churn-mixed), cost-mixed-7node
  (overlaps mixed-technology)
- Add churn-mixed scenario with --nodes flag for scale testing
- Reduce idle scenario durations: smoke-10 60s→30s,
  ethernet-only 90s→30s, cost-avoidance 120s→45s,
  depth-vs-cost 120s→45s, bottleneck-parent 120s→60s,
  mixed-technology 180s→90s

CI updates:
- ci-local.sh: unified image build, structured chaos suite entries
  with per-scenario flags, --skip-build for sidecar
- ci.yml: shared binary install + image build step, updated scenario
  matrix, chaos_flags support for parameterized scenarios
- Add tcp-mesh and congestion-stress to CI matrix
- Static ping test uses active peer convergence detection instead
  of hardcoded 5s sleep

Chaos infrastructure improvements (from discovery-rework branch):
- Pre-built Docker image instead of per-service build at scale
- --nodes flag in chaos.sh for runtime topology size override
2026-03-19 18:08:36 +00:00
Johnathan Corgan 0e098cadad Add top-level packaging Makefile and README 2026-03-17 14:57:01 +00:00
Johnathan Corgan ebabb6e93c Update changelog for post-v0.1.0 fixes and remove no-op clone 2026-03-17 14:45:29 +00:00
Johnathan Corgan 6e03adde72 Replace > 0 counter guards with has_prev_rr flag for delta computation
The loss and goodput delta guards used prev_rr_highest_counter > 0 and
prev_rr_cum_bytes > 0, which conflates "never received a report" with
"received a report where the value was 0." Since Noise transport
counters start at 0, this is semantically wrong. Use a dedicated
has_prev_rr boolean flag instead. Fixes GitHub issue #14 (Bug B).
2026-03-16 17:48:55 +00:00
Johnathan Corgan 1020f90828 Fix reverse delivery ratio using per-interval deltas instead of lifetime cumulative
The reverse delivery ratio was computed as cumulative_packets_recv /
highest_counter (lifetime values), while the forward ratio correctly
used per-interval deltas. This made ETX increasingly unresponsive over
session lifetime — early loss haunted the ratio forever, late loss
barely moved it.

Replace set_delivery_ratio_reverse() with update_reverse_delivery()
that tracks previous snapshot state and computes deltas, matching the
forward ratio's approach. Fixes GitHub issue #14 (Bug A).
2026-03-16 17:30:28 +00:00
Johnathan Corgan 0f24333c0d Fix post-rekey jitter spikes from drain-window frames
After rekey cutover, frames encrypted with the old session can still
arrive during the 10s drain window. These carry large sender timestamps
from the old session, producing enormous transit deltas that spike the
EWMA jitter estimator (observed 2,000-7,000ms spikes on UDP links).

Add a time-based grace period (DRAIN_WINDOW_SECS + 5s = 15s) after
rekey that suppresses jitter updates until drain-window frames have
flushed. Baselines still update every frame so calculation resumes
cleanly after grace expires.

Fixes #10
2026-03-16 12:41:41 +00:00
Johnathan Corgan ef73e54316 Update changelog with post-v0.1.0 bug fixes and additions 2026-03-16 02:39:01 +00:00
Johnathan Corgan 8697897047 Update changelog with post-v0.1.0 bug fixes and additions 2026-03-16 02:33:14 +00:00
Johnathan Corgan 9e11a8e7ba Return NOERROR for A queries on valid .fips names instead of NXDOMAIN
The DNS responder returned NXDOMAIN for non-AAAA queries (e.g. A record)
on valid .fips names. This caused resolvers like nslookup and tinyproxy
to give up without attempting AAAA, since NXDOMAIN means "name does not
exist." Return NOERROR with an empty answer section instead, which tells
the client the name exists but has no records of that type.

Fixes jmcorgan/fips#9.
2026-03-16 02:28:31 +00:00
Johnathan Corgan 5f7fe989f3 Fix rekey dual-initiation, parent selection, peer reconnect, and control socket
Rekey dual-initiation race (FMP + FSP):
When both sides' rekey timers fire simultaneously on high-latency links
(Tor ~700ms RTT), both msg1s cross in flight before dampening can
suppress the second initiation. Each node acts as both initiator and
responder, with set_pending_session() from the responder path destroying
the initiator's in-progress state. Each side ends up with a
pending_new_session from a different Noise handshake, causing AEAD
verification failure after cutover and link death. Fix: deterministic
tie-breaker in both FMP msg1 handler and FSP SessionSetup handler
(smaller NodeAddr wins as initiator). Also guard against pending session
overwrite from retransmitted msg1s.

Parent selection SRTT gate:
Peers without MMP RTT data are excluded from parent candidacy via
has_srtt() filter, preventing freshly reconnected peers from appearing
artificially attractive. First-RTT triggers immediate parent re-eval.
The gate in evaluate_parent() skips unmeasured candidates when any peer
has cost data, but falls back to default cost 1.0 during cold start
when no peer has MMP data yet.

Auto-reconnect on all peer removal paths:
The excessive-decryption-failure and peer-restart epoch-mismatch removal
paths were missing schedule_reconnect() calls, causing auto-connect peers
to be permanently abandoned after those events.

Control socket accessibility:
Socket and parent directory chowned to root:fips with mode 0770/0750 so
group members can use fipsctl/fipstop without root.

Log level changes:
Default RUST_LOG changed to debug in systemd service files. "Unknown
session index" log reduced from debug to trace.
2026-03-16 00:51:55 +00:00
Johnathan Corgan 324535e76d Make auto-connect peers retry indefinitely on initial connection failure
Previously, static peers configured with AutoConnect gave up after 6
attempts (1 initial + 5 retries). If the remote peer was offline at
startup, the node permanently abandoned the connection. The reconnect
path (after MMP link-dead) already retried indefinitely but only
activated after a peer had been previously connected.

Remove the reconnect parameter from schedule_retry() and always set
reconnect=true when creating retry entries, since only auto-connect
peers reach this code path. The 300s backoff cap prevents resource waste.
The max_retries=0 config still works as an explicit kill switch.
2026-03-15 18:43:11 +00:00
Johnathan Corgan 959134657f Fix FSP rekey cutover race and MMP metric discontinuity
The FSP XK rekey handshake has a race condition where the initiator
can cut over (K-bit flip) and send data encrypted with the new session
before msg3 reaches the responder. The responder has no pending session
yet, so K-bit detection fails and packets are dropped.

Defer FSP initiator cutover by 2 seconds after handshake completion
(FSP_CUTOVER_DELAY_MS) to give msg3 time to traverse the mesh. FMP
(IK, 2 messages) is unaffected since the responder completes during
msg1 processing.

Also fix MMP metric corruption after rekey cutover: the new session
starts with counter 0 but MMP state carries highest_counter and
GapTracker.expected_next from the old session, producing false
reorder counts, jitter spikes, and invalid OWD trends. Add
reset_for_rekey() methods that clear counter-dependent state while
preserving RTT estimates.

Additional fixes:
- Remove stale peers_by_index entry on abandon_rekey error path
- Replace redundant peers_by_index inserts with debug assertions
  verifying the pre-registration invariant
- Tighten rekey integration test to zero tolerance (was 4 failures)
2026-03-15 17:55:35 +00:00
Johnathan Corgan 6c90cf6c02 Implement Tor transport with operator visibility
Add TorTransport in src/transport/tor/ supporting three operating modes:

Outbound (socks5 mode):
- Non-blocking SOCKS5 connect via tokio-socks with per-destination
  circuit isolation (IsolateSOCKSAuth)
- TorAddr enum for .onion and clearnet address types
- Connection pool with per-connection receive tasks, reuses TCP
  stream FMP framing
- connect_async()/connection_state_sync()/promote_connection() follow
  the same non-blocking polling pattern as TCP transport

Inbound (directory mode — recommended for production):
- Tor manages the onion service via HiddenServiceDir in torrc
- FIPS reads .onion address from hostname file at startup
- No control port needed — enables Tor Sandbox 1 (seccomp-bpf)
- Accept loop mirrors TCP pattern with DirectoryServiceConfig

Monitoring (control_port mode and optional in directory mode):
- Async control port client supporting TCP and Unix socket connections
  via Box<dyn AsyncRead/Write> trait objects
- AUTHENTICATE with cookie or password auth
- 8 GETINFO queries: bootstrap, circuits, traffic, liveness, version,
  dormant state, SOCKS listeners
- Background monitoring task polls every 10s, caches TorMonitoringInfo
  in Arc<RwLock> for synchronous query access
- Bootstrap milestone logging (25/50/75/100%), stall warning (>60s),
  network liveness transitions, dormant mode entry
- Directory mode optionally connects to control port when control_addr
  is configured (non-fatal on failure)

Operator visibility:
- show_transports query exposes tor_mode, onion_address, tor_monitoring
  (bootstrap, circuit_established, traffic, liveness, version, dormant)
- fipstop transport detail view: Tor mode, onion address, SOCKS5/control
  errors, connection stats, Tor daemon status section
- fipstop table view: tor(mode) label with truncated onion address hint

Security hardening:
- Per-destination circuit isolation via IsolateSOCKSAuth
- Unix socket default for control port (/run/tor/control)
- Reference torrc with HiddenServiceDir, VanguardsLiteEnabled,
  ConnectionPadding, DoS protections (PoW + intro rate limiting)

Config:
- TorConfig with socks5, control_port, and directory modes
- DirectoryServiceConfig: hostname_file, bind_addr
- control_addr, control_auth, cookie_path, connect_timeout,
  max_inbound_connections

Testing:
- 69 unit + integration tests with mock SOCKS5 and control servers
- Docker tests: socks5-outbound (clearnet via Tor) and directory-mode
  (HiddenServiceDir onion service)

Documentation:
- Transport layer design doc: Tor architecture, directory mode
- Configuration doc: Tor config tables and examples
2026-03-15 16:19:54 +00:00
Johnathan Corgan c0f30d8fe8 Add DNS hostname support in peer addresses for UDP and TCP transports
Add resolve_socket_addr() with IP fast path and tokio::net::lookup_host()
fallback for DNS hostnames. Peer addresses can now use hostnames like
"peer1.example.com:2121" alongside IP addresses.

UDP transport adds a per-transport DNS cache (60s TTL) to avoid
per-packet resolution. TCP resolves at connect time (one-shot).

Update design docs, config examples, and changelog to reflect hostname
support in transport addressing.
2026-03-15 00:49:54 +00:00
Johnathan Corgan d873d0e00e Update design docs for non-blocking transport connect and cleanup stale references
- Rewrite TCP "Connect-on-Send" section to document non-blocking connect
  model (ConnectingPool, PendingConnect, poll_pending_connects)
- Add connect() and connection_state() to Transport trait surface
- Expand Connection Lifecycle section with ConnectionState enum
- Remove phantom TCP socks5_proxy field (removed from code, superseded
  by TorConfig)
- Fix "future Tor transport" references (stream reader already shared)
- Replace misleading "tor:" named TCP instance example
- Update fips-intro.md implementation status (TCP and Ethernet are
  implemented, not "under active design")
2026-03-13 16:37:00 +00:00
Johnathan Corgan 1bfb58845a Implement non-blocking transport connect for connection-oriented transports
TCP (and future Tor) transports previously established connections
synchronously inside send(), blocking the node's RX event loop during
TCP handshake. This is particularly problematic for Tor where SOCKS5
circuit establishment can take 30-120 seconds.

Add a non-blocking connect path:
- ConnectionState enum in transport layer (None/Connecting/Connected/Failed)
- connect_async() on TcpTransport spawns background TCP connect task
- connection_state_sync() polls task completion, promotes to pool
- TransportHandle gains connect() and connection_state() dispatch methods
- Node tracks PendingConnect entries for connection-oriented transports
- initiate_connection() defers handshake for connection-oriented transports
- start_handshake() extracted as separate method for deferred invocation
- poll_pending_connects() in tick handler polls and completes handshakes
- Failed connects trigger retry via schedule_retry()

Connectionless transports (UDP, Ethernet) are unchanged — connect()
is a no-op and connection_state() always returns Connected.

The existing connect-on-send path in send_async() is preserved as
fallback for reconnection after connection drops.

811 tests pass (6 new), clippy clean.
2026-03-13 03:21:21 +00:00
Johnathan Corgan 1898a7c390 Update repository URLs and finalize CHANGELOG for v0.1.0 release
Update all fips-network/fips references to jmcorgan/fips across
Cargo.toml, README, CONTRIBUTING, packaging, and config files.
Merge CHANGELOG Unreleased and 0.1.0-alpha sections into a single
0.1.0 release entry. Update README roadmap.
2026-03-12 16:51:34 +00:00
Johnathan Corgan 6ab8b35755 Implement FSP port multiplexing and IPv6 header compression
Breaking wire format change: DataPacket payloads inside the AEAD envelope
now carry a 4-byte port header [src_port:2 LE][dst_port:2 LE] before the
service payload. The receiver dispatches by destination port.

Port multiplexing:
- send_session_data() takes src_port/dst_port params, prepends port header
- New send_ipv6_packet() compresses IPv6 header and sends on port 256
- Receive path dispatches DataPackets by port: port 256 decompresses IPv6
  header from session context and delivers to TUN, unknown ports dropped
- Port constants: FSP_PORT_HEADER_SIZE (4 bytes), FSP_PORT_IPV6_SHIM (256)

IPv6 header compression:
- New ipv6_shim module with compress_ipv6()/decompress_ipv6() pure functions
- Strips src/dst addresses (32 bytes) and payload length (2 bytes) from each
  packet, preserving traffic class, flow label, next header, and hop limit
  as 6-byte residual fields
- Addresses reconstructed from session context on receive side
- Net savings: 29 bytes per packet (overhead 106 → 77 bytes)
- FIPS_IPV6_OVERHEAD constant (77 bytes), effective_ipv6_mtu() updated
- 16 unit tests for round-trip fidelity, field preservation, error cases

Documentation:
- fips-wire-formats: DataPacket port header, port registry, IPv6 shim
  format tables, updated encapsulation walkthrough and overhead budget
- fips-ipv6-adapter: FIPS_IPV6_OVERHEAD (77 bytes), updated MTU numbers,
  TUN reader/writer flow with compression steps, impl status
- fips-session-layer: port-based service dispatch section, data transfer
  description, impl status
- fips-intro: IPv6 adapter as port 256 service, node architecture updated
- fips-mesh-operation: packet size summary with compressed overhead
- DataPacket doc updated with port header and dispatch model
- session_wire.rs module doc: DataPacket Port Multiplexing section
2026-03-11 12:53:32 +00:00
Johnathan Corgan f37eb4b846 Fix documentation drift from recent feature additions
Update 9 documentation files to match current implementation:
- Add missing rekey config section (node.rekey.*) and host mapping
  section to fips-configuration.md
- Update Ethernet frame format from [type:1][payload] to
  [type:1][length:2 LE][payload] in wire-formats and transport docs
- Fix Ethernet effective MTU from interface-1 to interface-3
- Mark rekey as Implemented in mesh-layer and session-layer status tables
- Change TCP default port examples from 443 to 8443
- Add rekey, persistent identity, host mapping, mesh size estimation to
  README features and status sections
- Update chaos scenario count from 16 to 20, add rekey topology to
  static test docs
2026-03-11 03:11:12 +00:00
Johnathan Corgan b33d6531ce Fix fips-dns.service pulling in systemd-resolved and hanging on missing fips0
The fips-dns.service unit had three issues:

1. Wants=systemd-resolved.service caused systemd to start systemd-resolved
   on systems that weren't using it, breaking existing DNS by rewriting
   /etc/resolv.conf to the stub resolver at 127.0.0.53.

2. The ExecStart busy-wait loop for fips0 had no timeout, hanging forever
   if fips.service failed to create the TUN device.

3. Installers unconditionally enabled fips-dns.service regardless of whether
   systemd-resolved was present.

Fix by replacing Wants= with ConditionPathExists=/run/systemd/resolve (skips
cleanly if resolved isn't running), adding Requires=fips.service (won't start
without the daemon), bounding the fips0 wait loop to 30 seconds, and making
the installers conditional on systemd-resolved being active.
2026-03-10 19:19:24 +00:00
Johnathan Corgan 7d9e7f8e18 Update README peer example with actual test node npub and address 2026-03-10 15:44:10 +00:00
Johnathan Corgan 31c0b2c969 Tolerate transient rekey cutover failures in integration test
Phase 3 (post-first-rekey connectivity) can see brief disruptions while
links are mid-cutover. Add a 5s settle time after the rekey wait and
allow up to 4 transient pair failures in Phase 3. Phase 5 remains
strict, ensuring full recovery after the second rekey cycle.
2026-03-10 15:02:59 +00:00
Johnathan Corgan f907adad7b Fix control socket path mismatch between daemon and clients
fipsctl and fipstop checked XDG_RUNTIME_DIR before /run/fips/, causing
them to look in /run/user/<uid>/fips/ while the systemd-managed daemon
listens on /run/fips/control.sock. Reorder the fallback to check the
system-wide path first. Also improve the permission-denied error in
fipsctl to suggest adding the user to the fips group.
2026-03-09 14:37:26 +00:00
Johnathan Corgan aa53da061f Add local CI runner script and gitignore fipstop in test dirs 2026-03-09 03:14:58 +00:00
Johnathan Corgan 76ac1252d2 Enhance fipstop peers display with transport, direction, and tree roles
Add full npub column, transport type/address, direction (in/out),
goodput, and LQI-ascending sort to peers table. Colorize spanning
tree parent (magenta) and children (cyan). Add tree relationship
and transport info to show_peers JSON output.
2026-03-09 01:25:34 +00:00