mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
9d92cfeab4ea552e8c3f662fcd2cb1a3b8eee012
62
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0f27bdbd2c |
Give the UDP microbench a reason and record the retry asymmetry
The UDP recv microbenchmark carried a bare #[ignore] while its sibling in the link module carries a self-describing one. Match it, so the reason appears wherever the test is listed rather than only in the doc comment. Record why [profile.ci] retries = 2 is not applied locally. The two gates disagree on purpose: the hosted runner retries a flaky test twice, the local sweep fails on the first failure. It exists for shared-runner packet loss, a property of that environment rather than of the code, and applying it locally would suppress a real local flake — a failure that only reproduces under load is a robustness bug to fix, not to retry past. Keeping the local sweep strict is what makes it the sharper gate. An undocumented asymmetry is indistinguishable from an oversight, which is why this is written down rather than left to be rediscovered. The cost is stated rather than hidden: a test that fails once and passes on retry is reported green with no separate signal, so a genuine intermittent failure can be absorbed. If that starts mattering the fix is to surface retried-but-passed tests, not to drop the retries. Quartet clean: fmt, build --workspace, clippy --all-targets -D warnings, and test --lib at 1376 passed / 0 failed / 7 ignored. |
||
|
|
3d771c6688 |
Wire the Tor transport connect_refused counter
The connect_refused stat counter (the Refused line in fipstop) was defined but never incremented: every SOCKS5 connect failure recorded socks5_errors instead, so the counter sat at zero and the operator-facing gauge was permanently misleading. Both the synchronous connect path and the background connect_async task now count a genuine SOCKS5 REP=0x05 refusal as connect_refused and every other failure as a socks5_error, distinguishing them precisely via tokio_socks::Error::ConnectionRefused. Extends the mock SOCKS5 server with a configurable reply code and adds two tests covering the refused and general-failure paths. |
||
|
|
4e43cb81e9 |
Add Nym mixnet transport and single-container demo
Add an outbound-only Nym mixnet transport that tunnels FMP peer links through a local nym-socks5-client SOCKS5 proxy into the Nym mixnet. It structurally mirrors the Tor SOCKS5 transport (connection pool, connect-on-send background promotion, FMP-v0 framing reused from TCP) with the onion, inbound-listener, and control-port machinery removed. Wires the transport through the full TransportHandle dispatch, NymConfig (standard transport-instance pattern), and node instantiation, and surfaces its counters in fipstop. Includes a mock SOCKS5 harness and unit coverage for the address-parsing paths. Also adds an isolated single-container example (examples/sidecar-nostr-mixnet-relay/) demonstrating FIPS peering across the mixnet end to end. No new crate dependencies: tokio_socks, socket2, and futures are already pulled in by the Tor transport. |
||
|
|
5fc2359432 |
fipstop: TUI overhaul with render-snapshot harness and navigation model
Reworks the fipstop TUI across its rendering, the control read surface it draws from, and its interaction model, on a machine-verified base. Test infrastructure: - Add a ratatui TestBackend snapshot harness (testkit + snapshots modules) that renders any ui::draw_* into an in-memory Buffer from canned show_* JSON and asserts the text grid plus per-cell style. Layout, columns, alignment, labels, grouping, and colour are now checkable under cargo test; every render below ships a snapshot. Control read surface (each new field emitted byte-identically on the live and off-loop builders, published once from the tick, with schema fixtures regenerated and the parity asserts holding): - show_status: effective persistence (persistent || nsec.is_some()); root and is_root; and a per-configured-transport-type peer-count map in which idle-but-configured types stay visible at zero. - show_peers: per-peer effective_depth (depth + link_cost, the value evaluate_parent ranks on), null when unmeasured or coordless so fipstop never recomputes it. - show_tree: root_npub, resolved once daemon-side (self when root, an attested peer npub, or an identity-cache hit). - show_bloom: the last-actually-sent uptree filter fill ratio and subtree estimate, null for a root or before the first announce. - show_mmp: session-layer srtt, loss, and etx trend labels. Rendering: - Display a 6-byte non-UTF-8 TransportAddr as a colon-separated MAC at the type layer, so daemon logs, fipsctl, and JSON consumers all benefit; non-6-byte payloads stay bare hex. - Right-justify the Bloom Peer Filters numerics into aligned fixed-width columns, render the Routing panes through a kv_lines helper that shares one value column across a key-value group, and right-justify the Graphs by-peer summary columns. - Truncate an over-long peer name (the npub shown when no friendly name exists) in the Tree, Bloom, and MMP peer lists so it no longer runs into the next column. - Group the Peers table by role (parent, then STP children, then other) and render it as a full grouped view with styled group labels and blank separators; the selection stays a peer index and the cursor only ever lands on a peer row. Apply the same role grouping to the Tree and Bloom peer lists, joining each peer's role from the peers view by node address. - Show min in the Graphs plot titles, rest a steady non-zero metric on the baseline as a row of dots, render a genuine zero as an empty plot, and keep a distinct no-data placeholder. - Replace the metric-by-peer grid, which squeezed plots to nothing once peers overflowed, with a master/detail Graphs view: a scrollable per-peer summary list that expands (Enter) to a full-pane btop plot, with up/down to flip peer, n/N to switch statistic, m to cycle mode, and Esc to return. - Put inline colored trend arrows on the Link and Session MMP values (drawn only on a rising or falling trend, with a fixed blank slot when stable so the value columns stay aligned), via a shared helper. - Cycle column sorting on the Link MMP, Session MMP, and Graphs by-peer tables (one key cycles the active column, another toggles direction), with the active column marked in each table's header. - Render the new daemon-surfaced fields: the dashboard root line (a self-is-root marker, otherwise a truncated root hex), a transports-by-type line, and an "approx. mesh estimate" line; an effective_depth column and lines on the Peers, peer-detail, and Tree sites from the single daemon derivation, showing a dash placeholder when unmeasured rather than a misleading zero; the full Tree root hex plus an Npub line; and the Bloom uptree fill and subtree-estimate lines. Interaction model: - Add a declarative keybinding registry keyed by (Tab, UiMode) that both the context footer and the ? help overlay render from, so the two cannot drift; a test asserts every registry key has a dispatch handler. - Add a modal ? help overlay, and a context-aware footer that shows the current state's actions first, drops global hints when the terminal is narrow, and always keeps a Help affordance as the overflow path. - Generalize per-pane focus and scroll state on App, wired across the Tree, Filters, Routing, and MMP tabs (f cycles pane focus and the focused pane scrolls instead of clipping its overflow); on the MMP tab the column sort acts on the focused pane. Esc deselects the active row when no detail is open (detail-close still takes priority). - Add a Del-disconnect confirmation modal naming the peer, the only state-mutating action, issuing the control-socket disconnect on confirm and noting that the peer stays disconnected until manually reconnected. |
||
|
|
bdf571a2b2 |
Merge branch 'maint'
# Conflicts: # src/node/mod.rs |
||
|
|
2eea20a216 |
tcp: drive inbound connection cap from node.limits.max_connections
The per-transport TCP inbound cap was hardwired to 256 and never read node.limits.max_connections, so raising max_connections was a silent no-op for inbound TCP. Resolve the effective cap with precedence: explicit per-transport max_inbound_connections, then node-wide max_connections, then the built-in default of 256. Established peers remain bounded node-wide by add_connection, so deriving the per-transport raw-accept ceiling from max_connections does not admit more real peers across multiple transports. Add effective_max_inbound on the TCP transport with a node_max_connections setter wired from create_transports, plus a precedence unit test. |
||
|
|
79b945b93d |
Merge maint into master (mesh-size OR-union estimate; log-level hygiene)
Brings the maint-line OR-union mesh-size estimator fix and the per-peer / capacity-cap log-level demotions forward to master. Conflict resolution: compute_mesh_size resolved to master's config() accessor form carrying maint's OR-union rewrite (drop the stale summing `total`); the log-level demotions auto-merged into master's handler versions. The equivalent master-only log "connected UDP socket installed" (connected_udp.rs, a file that does not exist on maint) was demoted info -> debug as part of this merge so the connected-UDP path matches the rest of the per-peer lifecycle logging. |
||
|
|
974e146bb9 |
node: demote routine per-peer and capacity-cap events from info/warn to debug
On a saturated public-mesh node the connection-lifecycle and capacity-cap events fire continuously and drown out the genuinely notable INFO/WARN lines. Demote them to debug and drop a redundant duplicate: - FMP K-bit cutover promotion (encrypted): info -> debug - "Connection promoted to active peer" (handshake): info -> debug, and remove the duplicate "Inbound peer promoted to active" line that shadowed it on the inbound path - "Peer restart detected" (handshake): info -> debug - "Peer removed and state cleaned up" (dispatch): info -> debug - "Rejecting inbound TCP connection (max_inbound_connections reached)" (tcp): warn -> debug - "Congestion detected, CE flag set on forwarded packet" (forwarding): warn -> debug - "Removing peer: link dead timeout" (mmp): warn -> debug These are expected, high-frequency conditions on a busy public node (new and reconnecting peers, ECN CE marking, the inbound connection cap, and link-dead churn), not operator-actionable signals. |
||
|
|
e5372cbe0f | Merge maint into master (libclang build-prereq doc + transport mutex-poison recovery) | ||
|
|
9dcc421f6f |
transport: recover poisoned mutex guards instead of panicking on lock
The transport layer used Mutex::lock().unwrap() at ten sites across the UDP, BLE, and Ethernet code. A std mutex poisons if a thread panics while holding it, after which every lock().unwrap() on that same mutex also panics, turning one fault into a cascade. These critical sections only perform short HashMap/Vec operations on locally constructed values and are not reachable from peer input, but the idiom is fragile against any future in-section panic. Replace each with lock().unwrap_or_else(|e| e.into_inner()), which recovers the guarded data and removes the cascade with no new dependency and no call-graph change. Also replace four self.local_addr.unwrap() calls in the UDP start and adopt paths with a sentinel fallback. The value is provably set just above each log line today, but the unwrap is brittle against a future reordering; logging an unbound sentinel is harmless and cannot panic. |
||
|
|
e7349202b5 | Merge branch 'maint' into master | ||
|
|
de327e4527 |
test: run node-level mesh tests over an in-process loopback transport
Add a Loopback variant to TransportHandle backed by an unbounded in-process channel and a shared address-to-receiver registry, so node-level multi-node tests deliver packets directly between nodes instead of over real localhost UDP sockets. This removes the kernel UDP receive-buffer overflow that dropped handshake packets when many tests ran in parallel under CPU contention, and lets the large-network convergence tests run reliably in the default suite again (their parallel-load ignore markers are removed). The new transport and its enum variant are cfg(test)-gated, so the daemon build is unaffected. |
||
|
|
2d0e8de8c8 |
transport/udp: detach peer-drain worker thread on Drop to avoid runtime-driver deadlock
PeerRecvDrain::drop previously called std::thread::join on the worker
thread synchronously. The worker uses packet_tx.blocking_send on a
tokio mpsc Sender, which internally parks the worker via
tokio::block_on on the same current_thread runtime that drives
rx_loop. Calling join from inside remove_active_peer (which runs on
the runtime thread, the runtime's sole driver) created a circular
wait:
- rx_loop blocks in libc futex via Thread::join
- the worker being joined cannot observe the stop flag because the
runtime that polls it is the very thread now blocked joining it
- all other PeerRecvDrain workers park on the same runtime via
block_on, so a single peer's removal wedges every worker on the
daemon
The /proc snapshot from a production wedge showed exactly this
shape: 107 of 108 threads in futex_do_wait, 101 of them named
fips-peer-drain. fipsctl became unresponsive (EAGAIN on control
socket), SIGTERM was ignored, and Docker SIGKILLed the container
after the 10 s grace period. Two confirmed wedges on the public
test deployment (52 min and 23 min uptime), plus a third on the
admission-gate-Msg2-silent-drop build at 2 min 21 sec — all ending
with the identical "Peer removed and state cleaned up
tree_changed=false" final log line preceding total silence.
Fix: detach the std::thread instead of joining. The stop flag plus
self-pipe write already signal the worker to exit; the worker's
kernel-level libc::poll inside the drain loop sees the wake, checks
the flag, exits, and the OS reclaims the thread state independently
of the JoinHandle being dropped.
The trigger was statistically amplified by aggressive multi-npub-
from-one-NAT peer reconnect patterns at the moment of the 30 s
link-dead-timeout peer-removal, but not bounded to them. Any
peer whose disconnect happens with the per-peer drain worker
parked in block_on can fire the bug. The admission-gate work
that landed earlier in this branch line compressed more handshake
work per rx_loop tick, increasing the rate at which workers are
parked in block_on and so reducing time-to-wedge — but the
underlying bug pre-dated the admission gate and pre-dated this
fix branch.
The deployed wedged daemon is mitigated operationally by blocking
the trigger IP at the host firewall; this commit removes the bug
class entirely.
|
||
|
|
8d94c0f29c | Merge branch 'maint' | ||
|
|
6dee6dfe27 |
transport: cap max_inbound_connections on inbound count, not combined pool
Add per-direction pool_inbound/pool_outbound counters to TcpStats and TorStats, updated at every pool-insert, receive-loop-exit, transport-stop, and send-failure removal site. Compare the max_inbound_connections cap against pool_inbound rather than the combined pool length, so outbound connect-on-send connections no longer consume the operator-facing inbound budget. The configuration field name and operator semantics are preserved; only the cap-check comparison and accounting change. |
||
|
|
2809f0351e | Fix connected UDP drain poll error spin (#106) | ||
|
|
0a5c367edc |
data-plane perf overhaul: off-task encrypt + decrypt, GSO, connected UDP
Moves both AEAD layers (ChaCha20-Poly1305, one round per layer per packet) plus the sendmsg syscall off the rx_loop task onto a per-shard worker pool, adds per-peer connect(2)-ed UDP with SO_REUSEPORT, and uses Linux UDP GSO (sendmsg+UDP_SEGMENT — kernel splits one super-skb into N on-the-wire datagrams in a single TX-stack walk) when packets in a batch are uniform-size. Same kernel primitive WireGuard's in-kernel module and BoringTun use to hit 2.5–3.2 Gbps single-stream. Single TCP stream on a 5-node docker-bridge mesh, 5 x 15 s x P=1: A→D: 1379 → 2708 Mbps (1.96x, RTT +0.12 ms) A→E: 1394 → 2663 Mbps (1.91x, RTT +0.11 ms) E→A: 1406 → 2624 Mbps (1.87x, RTT +0.19 ms) Static-peer pairs only — every CoV under 3%, 0 outliers, 0% ICMP loss. The ~+100 µs RTT is the worker queue handoff cost; AEAD + sendmmsg now run on a separate core in exchange. What lands: - src/node/encrypt_worker.rs: std::thread + crossbeam_channel workers; hash-by-destination dispatch pins a TCP flow to one worker so wire ordering is preserved; per-worker sendmmsg(2) batching up to 32; Linux uses sendmsg(2)+UDP_SEGMENT when packets in a group are uniform-size. - src/node/decrypt_worker.rs: receive-side mirror. Each shard owns its session's recv cipher + replay window in a thread-local HashMap (no shared RwLock/Mutex). Sessions are handed off at promote_connection and re-registered on K-bit flip / rekey cutover. - src/node/handlers/session.rs try_send_session_data_pipelined: FSP+FMP both seal in-place in the worker on one wire-buffer alloc; no intermediate inner_plaintext / fsp_payload Vecs. - src/transport/udp/connected_peer.rs + peer_drain.rs: per-peer connect(2)-ed UDP socket with SO_REUSEPORT (set on the listen socket too — without that, EADDRINUSE on activation and every packet falls back to the wildcard path); the worker sends with msg_name=NULL and the kernel uses its cached 5-tuple. Tick- driven activation in handlers/connected_udp.rs, idempotent. - src/transport/udp/mod.rs: mem::replace the recvmmsg backing buffer instead of buf.to_vec() per packet — single pointer swap, no MTU-sized memcpy. - src/protocol/link.rs SessionDatagramRef: zero-copy borrowed view used by handle_session_datagram for the bulk local-delivery path; handle_session_payload takes the borrowed payload directly (no payload[35..].to_vec()). - src/transport/mod.rs TransportAddr::from_socket_addr: collapses the two-alloc from_string(addr.to_string()) pattern to one. - src/node/handlers/rx_loop.rs: decrypt-fallback drain promoted ahead of packet_rx in the select! (TCP ACK starvation fix); interleaved fallback drain every 32 packets inside the rx burst loop. - noise::Session: send_cipher_clone / recv_cipher_clone / recv_replay_snapshot_owned / take_send_counter / accept_replay so off-task workers can hold a cloned cipher + reserved counter while the dispatcher keeps replay/counter sequencing serial. CipherState::cipher_clone returns a refcount-bumped LessSafeKey. AsyncUdpSocket: AsRawFd so workers issue raw sendmmsg / sendmsg without going through the tokio reactor. - Worker pool sizing: both default to num_cpus, overridable via FIPS_ENCRYPT_WORKERS=N / FIPS_DECRYPT_WORKERS=N. Per-peer connected UDP can be disabled via FIPS_CONNECTED_UDP=0. - src/perf_profile.rs: optional per-stage timing reporter under FIPS_PERF=1 (or FIPS_PIPELINE_TRACE=1). Off by default; zero overhead when disabled. - All cfg(unix)-gated. Windows continues on the existing tokio- based send/recv. Decrypt worker session lifecycle: - Node::unregister_decrypt_worker_session mirrors the existing register helper. Wired at the two natural sites that already iterate peers_by_index: the rekey drain-completion block in handlers/rekey.rs (drops the worker entry for the old our_index once the drain window has expired and the cache_key is unreachable to any in-flight OLD-K packet), and remove_active_peer in handlers/dispatch.rs (drops the worker entry for each of the four index slots: current, rekey, pending, previous). Only our_index is normally registered; unregister_session is fire- and-forget for missing entries, so calling unconditionally on all four slots is correct and bounds the cleanup without per- slot accounting. Without these callers the per-worker sessions HashMap and the Node's decrypt_registered_sessions set would grow monotonically per rekey on long-lived peers. Testing: - testing/static/scripts/bench-multirun.sh: multi-run iperf3 + ping bench. N reruns (default 5), median / min / max / CoV % / per-run outlier flag, avg ping RTT, ICMP loss %, TCP retransmit total. Plain client→dest labels + topology header. Pre-bench peer-convergence check (FIPS_BENCH_CONVERGE_SECS, default 15); per-path route verification via stats.bytes_sent deltas — fails fast if traffic exits via a non-static-peer link. - testing/static/docker-compose.yml: passes FIPS_ENCRYPT_WORKERS / FIPS_DECRYPT_WORKERS / FIPS_PERF through to containers for A/B benchmarking without rebuilds. - testing/static/scripts/iperf-test.sh: same plain client→dest labels + topology header (was multihop/direct/N hop, which conflated topology distance with on-wire path). - .config/nextest.toml: synthetic UDP node tests serialized through a max-threads=1 test group. Localhost handshakes drop on shared CI runners under parallel load; one-at-a-time keeps assertions reliable. - src/node/tests/spanning_tree.rs: repair_missing_edge_handshakes — retries up to 5 times for synthetic edges whose msg1 was dropped, with a drain after each edge retry instead of after each attempt's full burst. - src/node/decrypt_worker.rs::tests: two unit tests asserting WorkerMsg::UnregisterSession removes the worker-thread session HashMap entry (handle_msg_unregister_session_removes_entry) and is a no-op for never-seen cache_keys (handle_msg_unregister_session_idempotent_on_unknown_key), which is the safety invariant the unconditional unregister calls at the four index slots in remove_active_peer rely on. - src/node/encrypt_worker.rs::unix_tests pipelined_send_wire_layout_roundtrips_canonical_decoders: mirrors the encoder geometry of try_send_session_data_pipelined (no coords, the common established-session path), runs the worker's real seal + send via flush_direct_batch_sync, and decodes the resulting wire packet using only canonical receive-side decoders (EncryptedHeader::parse, SessionDatagramRef::decode, FSP header parse, noise::open). Any divergence between the hand-rolled encoder offsets (fsp_aad_offset, fsp_plaintext_offset) and the decoders fails at one of the parse / open / decode steps before the inner-plaintext assertion fires. Complements the existing fsp_preseal_runs_before_outer_fmp_seal test which covers the seal-ordering invariant with synthetic headers but does not exercise the wire-layout invariant. CHANGELOG.md [Unreleased] # Changed entry added describing the worker-pool threading model, hash-by-destination dispatch, sendmmsg/UDP_GSO, per-peer connected UDP, the operator-facing env vars, and the bench numbers above. Cherry-picks from mmalmi/master (paths translated from crates/fips-core/src/ to src/): 9b7c723, 0deb5cb, 13f7339, e036c0e, 3740a68, 3792f83, 8510193, 4910b07, e53f545, e4e2896, 5fe4af5, 1d01ada, 8c37008, e12469e, 6eb2860. Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com> |
||
|
|
6bd40640bf |
chore: quiet platform-specific warnings
The non-Linux test build was emitting warnings from code that is intentionally platform-specific: the nftables firewall parser is Linux-only, the utun address-family helper is only used in macOS TUN paths, and one macOS Ethernet test module trips a clippy layout lint. These warnings made focused test runs noisy and encouraged bundling unrelated warning fixes into behavioral PRs. - Gate the firewall parser dead-code allowance to non-Linux targets, where the parser is compiled but not used. - Mark the macOS utun helper and long TUN reader entry point with narrow allowances. - Rewrite the small MAC-copy loop to satisfy clippy and mark the macOS Ethernet test module layout explicitly. No runtime behavior change. |
||
|
|
b1af151aef |
rx: avoid copies in receive hot paths
- Borrowed SessionDatagramRef decoder is used in the forwarding
handler so local delivery and coordinate-cache warming no longer
allocate or copy the session payload. The owned SessionDatagram is
materialized only when re-encoding for the next hop.
- Owned SessionDatagram::decode is reimplemented as Ref::decode +
into_owned, so the two decoders cannot drift.
- recvmmsg / recvmsg_x (Linux + macOS) receive loop moves each filled
slot buffer into ReceivedPacket via mem::replace instead of cloning
it; a fresh empty buffer is installed for the next syscall.
- TransportAddr is formatted directly from the SocketAddr without
going through an intermediate String.
Focused decode bench: ref 1.6 ns/op vs owned 34.7 ns/op (21.4x).
End-to-end iperf is neutral as expected for a ~30 ns saving per
packet.
Unit tests added:
- test_session_datagram_ref_decode_borrows_payload (verifies the
payload slice pointer equals the input slice's offset 35, a real
zero-copy invariant guard against accidental future to_vec)
- bench_session_datagram_decode_owned_vs_ref (ignored, run with
--ignored --nocapture)
- test_transport_addr_from_socket_addr
|
||
|
|
59225ccfe1 |
udp: batch macOS receive with recvmsg_x
The Linux recv path drains up to 32 datagrams per kernel wakeup via recvmmsg(2), amortising the per-syscall + per-task-wakeup cost across the burst. macOS still fell through to single-packet recv_from, so the same overhead capped inbound rate on Apple builds. Add an equivalent batch path for Darwin using recvmsg_x(2). It is a xnu-private syscall (not in the public SDK) but is the canonical amortisation primitive on macOS — same shape used by quinn-udp for the same reason. ABI is the public msghdr layout plus a trailing msg_datalen (per-datagram bytes-received output), declared via `unsafe extern "C"` against a local repr(C) `msghdr_x`. Same `(count, kernel_drops)` contract as the Linux `recv_batch`. macOS has no SO_RXQ_OVFL equivalent, so `kernel_drops` is always 0 — the 1Hz `sample_transport_congestion()` detector simply sees no kernel drop signal on Apple hosts (it already tolerates that, since the field has been 0 there pre-batching too). cmsg buffer is intentionally null: we never consume ancillary data on this path, and quinn-udp documents that `recvmsg_x` does not overwrite `msg_controllen` on macOS 10.15+ (zeroed init is the only safe state). The udp_receive_loop dispatch widens from cfg(linux) to cfg(any(linux, macos)); the per-packet recv_from path is now used only on the remaining unix targets (BSDs etc.) and Windows. Add test_burst_recv_batch exercising 10 in-flight datagrams to verify per-datagram boundaries and arrival order across the batch. Add an ignored bench_udp_recv_amortization measuring recv-side syscall amortization across 1/2/4/8 sender threads on dedicated blocking std threads (kernel rx queue stays saturated regardless of tokio scheduling). Sample numbers on aarch64-apple-darwin (100B payloads, 3s windows): senders=1: recv_from 398k pps recv_batch 432k pps 1.09x senders=2: recv_from 353k pps recv_batch 608k pps 1.72x senders=4: recv_from 322k pps recv_batch 503k pps 1.56x senders=8: recv_from 353k pps recv_batch 515k pps 1.46x Gate the Linux-only IpAddr import in control::listening behind a cfg(target_os = "linux") so the macOS test build is warning-clean now that test code paths there compile. |
||
|
|
253dddabe3 |
udp: batched recvmmsg receive on Linux (32-pkt bursts)
The UDP recv loop drained the kernel queue one packet per recvmsg(2). Each call paid full per-syscall + per-task-wakeup overhead (~50us avg including a futex-based scheduler hop), so under sustained load the loop ran at one rx event per scheduler quantum — the dominant cap on inbound packet rate. On Linux, switch the steady-state path to recvmmsg(2) with a 32-packet batch. A single readable() wakeup drains up to 32 datagrams in one syscall before yielding back to the reactor. Stack-allocated mmsghdr arrays sized to a module-level `BATCH_SIZE` constant. `SO_RXQ_OVFL` is sampled once per batch off the cmsg chain of `msgs[0]` and plumbed through `AsyncUdpSocket::recv_batch` as `(count, drops)`. The counter is socket-wide and monotonic, so a single sample per batch gives the 1Hz `sample_transport_congestion()` detector ample fresh values under load (one batch = up to 32 datagrams). Cost is one stack-allocated CMSG_SPACE(4) buffer + one CMSG_FIRSTHDR walk per batch syscall. macOS / Windows fall through to the per-packet recv_from loop — recvmmsg is Linux-specific and the per-packet API is fast enough on those platforms for now (recvmsg_x for Darwin can be added later). The slice-array build also drops the `MaybeUninit::uninit().assume_init()` + `transmute` pair for `std::array::from_fn` over a single shared `backing.iter_mut()` — same disjoint mutable borrows, no `unsafe`. |
||
|
|
f32bc83034 |
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. |
||
|
|
9204888a54 |
Pin macOS utun AF prefix and BPF frame parsing at unit level
Add #[cfg(target_os = "macos")] unit tests catching macOS-specific regressions before they reach the macos-latest GitHub runner. src/upper/tun.rs: surgical refactor extracts the inline AF_INET6_HEADER constant into module-scope helpers utun_af_inet6_header() (encode) and parse_utun_af_prefix() (decode inverse for round-trip testability). TunWriter::run now calls the helper instead of the inline const; behavior unchanged. Six new tests pin the AF=30 constant matching Darwin, big-endian byte order, encode/parse round-trip, short-buffer rejection, minimum header acceptance with trailing payload, and no-panic on garbage bytes. src/transport/ethernet/socket_macos.rs: existing test mod already covered bpf_wordalign and 5 parse_next_frame cases. Three new tests fill genuine gaps: struct layout pin against kernel ABI, caplen- overrun rejection, full Ethernet header round-trip via parse. Existing tests pre-exist; only adding to the same #[cfg(test)] block. |
||
|
|
239cbdc4ba |
Fix Tor onion adverts missing port in Nostr overlay discovery
The Nostr overlay advert publisher serialized `transport: tor` endpoints as a bare `<onion>.onion` hostname with no port. The Tor address parser requires `<host>:<port>` form and rejected the bare shape with `expected host:port`. Any peer receiving a Tor-only advert went into a persistent retry-fail loop on jittered backoff until the advert aged out of the discovery cache. The bug had been latent for as long as Tor adverts have been published on Nostr, and was masked in deployments where every node also advertised a non-Tor transport (peers fell through to the working endpoint). Surfaced first on a deployment where Tor was the only advert path. Publisher now emits `<onion>.onion:<port>` using a new `transports.tor.advertised_port` config field that defaults to 443, matching the Tor `HiddenServicePort 443 127.0.0.1:<bind_port>` convention. Operators whose torrc uses a non-default virtual port can override. Adds a unit test that pins the publisher/parser contract: formats the advert exactly as the publisher does and asserts `parse_tor_addr` accepts the result; asserts the bare-onion form (the bug) does not parse, catching any future regression that drops the port again. Parser is unchanged (already correct). |
||
|
|
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. |
||
|
|
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. |
||
|
|
34e00b9f6e |
Add Nostr-mediated overlay discovery and UDP NAT traversal (#53)
Optional peer discovery and NAT hole-punching path gated behind a new
`nostr-discovery` cargo feature. Nodes publish signed overlay endpoint
adverts to public Nostr relays, consume peer adverts to populate
fallback dial addresses, and use STUN-assisted UDP hole punching with
NIP-59 gift-wrap offer/answer signaling to establish direct UDP paths
between NATed peers. Once a punched socket is up, it is handed into
the existing FIPS UDP transport and the standard Noise/FMP session
stack takes over unchanged.
The cargo feature is in the default feature set
(`default = ["nostr-discovery"]`) so stock builds include it; a
build that explicitly disables default features (or selects a
feature set without `nostr-discovery`) does not link the nostr /
nostr-sdk crates and does not emit a no-op poll in the tick loop.
Runtime behavior is independently gated by
`node.discovery.nostr.enabled`, which defaults to false; if the
config enables Nostr on a non-feature build, startup logs a
warning and continues without it.
== Cargo feature and dependencies
- New cargo feature `nostr-discovery = ["dep:nostr", "dep:nostr-sdk"]`.
Not in the default feature set.
- New optional Linux-only dependencies: `nostr 0.44` (features: std,
nip59) and `nostr-sdk 0.44`. Gift-wrap unwrap is hand-rolled in
`src/discovery/nostr/signal.rs` rather than relying on the SDK's
rumor-author check, which FIPS sidesteps by trusting `seal.pubkey`
exclusively.
== Wire format
Overlay advert event: `kind 37195`, parameterized replaceable
(NIP-01 application-defined replaceable range 30000-39999), with
`d = "fips-overlay-v1"`. The digits visually spell FIPS (7=F, 1=I,
9=P, 5=S); a relay survey confirmed the kind is unused.
Advert content carries the version tag, endpoint list
(`udp|tcp|tor` + addr), optional signal-relay and stun-server
metadata, and `issuedAt` / `expiresAt` timestamps. Endpoint
`addr: "nat"` is the sentinel that triggers traversal on the peer
side. NIP-40 `expiration` tag bounds staleness on permanent
shutdown. Lifecycle relies on parameterized-replaceable
supersession; the daemon does not emit NIP-09 kind-5 deletes —
strict relays (Damus, Primal) race delete-against-replace and can
silently drop the replacement.
Gift-wrapped signal event: `kind 21059`. Punch packets carry magic
values `PUNCH_MAGIC` / `PUNCH_ACK_MAGIC`, a sequence number, and a
16-byte session hash.
== Discovery surface
- `src/discovery.rs` (always compiled)
- `EstablishedTraversal`: bound UDP socket + selected remote +
peer npub + optional transport name/config tuning overrides.
- `BootstrapHandoffResult`: returned on successful handoff —
allocated transport id, local/remote addrs, peer NodeAddr,
session id.
- `src/discovery/nostr/` (`#![cfg(feature = "nostr-discovery")]`)
- `types.rs`: wire and control types described above. `ADVERT_KIND`
constant. `BootstrapError` enumerates failure modes (disabled,
missing advert, missing NAT endpoint, no usable relays, invalid
advert, invalid npub, signal timeout, punch timeout, replay,
STUN failure, protocol, nostr, io, serde, event-parse).
- `runtime.rs`: `NostrDiscovery` coordinator. Owns the shared
nostr-sdk `Client`, subscribes to advert + signal event kinds,
maintains a bounded advert cache and a bounded seen-sessions
replay set, drains `BootstrapEvent::{Established, Failed}` for
the node to consume, exposes `update_local_advert`,
`request_connect`, `advert_endpoints_for_peer`,
`cached_open_discovery_candidates`, and `shutdown`.
- `signal.rs`: NIP-59 gift-wrap encode/decode. Outbound wraps are
built against per-attempt ephemeral keys; inbound events are
unwrapped against the node identity.
- `stun.rs`: RFC 5389/8489 Binding Request client with
XOR-MAPPED-ADDRESS parsing for both IPv4 and IPv6; used only to
observe the initiator's own reflexive address against its
locally configured STUN list (peer-advertised STUN is
informational, never an egress target).
- `traversal.rs`: per-attempt candidate-pair punch planner.
Allocates a fresh `0.0.0.0:0` UDP socket per attempt, enumerates
LAN-private and ULA interface addresses alongside the STUN
reflexive address, schedules probe/ack exchanges at the
configured interval for the configured duration, and picks the
first candidate pair that authenticates end-to-end.
Strategy ordering is Reflexive↔Reflexive first, then LAN, then
Mixed. The STUN-observed pair is the only candidate that's reliable
across arbitrary network topologies; trying it first prevents the
planner from latching onto a misleading host-candidate path before
the reflexive path gets a chance. There is no catch-all
Local↔Local strategy: a previous design that paired every local
host candidate from one side with every local host candidate from
the other could declare success on a one-way reachable asymmetric
L3 path (corporate VPN, Tailscale subnet route, overlapping private
address space), only for the FMP handshake to stall because the
return path didn't match. The legitimate `Lan` strategy still pairs
candidates that share a subnet.
== Configuration surface
`node.discovery.nostr.*` (`NostrDiscoveryConfig`), all `serde(default)`
with `deny_unknown_fields`:
- `enabled` (default false), `advertise` (default true)
- `advert_relays`, `dm_relays`, `stun_servers`: defaults are
`wss://relay.damus.io`, `wss://nos.lol`, `wss://offchain.pub`
for both relay lists, and Google / Cloudflare / Twilio for STUN.
Operators are expected to override for production. Other
verified-working public relays for reference:
`nostr.bitcoiner.social`, `nostr-pub.wellorder.net`,
`nostr.oxtr.dev`, `nostr.mom`.
- `app` (default `"fips-overlay-v1"`), `signal_ttl_secs` (120)
- `policy`: `NostrDiscoveryPolicy::{Disabled, ConfiguredOnly (default),
Open}` — controls whether advert-derived endpoints are consumed
only for peers carrying `via_nostr = true`, or also for
non-configured peers within a budget cap.
- `share_local_candidates` (default false) — when false, the offer's
`local_addresses` list is empty and peers see only the reflexive
address. Enable per-node only for genuinely same-LAN deployments;
off-by-default eliminates the misleading-path failure mode for
the common case where peers are not on the same broadcast domain.
- `open_discovery_max_pending` (64) — caps queued open-discovery
retries; bounded by available outbound slots.
- `max_concurrent_incoming_offers` (16) — semaphore against offer
spam; excess offers are debug-logged and dropped.
- `advert_cache_max_entries` (2048) and `seen_sessions_max_entries`
(2048) — bound memory under ambient relay volume; overflow
evictions are debug-logged.
- `attempt_timeout_secs` (10), `replay_window_secs` (300)
- `punch_start_delay_ms` (2000), `punch_interval_ms` (200),
`punch_duration_ms` (10000)
- `advert_ttl_secs` (3600), `advert_refresh_secs` (1800)
Per-peer and per-transport flags:
- `PeerConfig.via_nostr: bool` — when true (and Nostr is enabled),
advert-derived addresses are appended as fallback dial candidates
after static addresses for that peer.
- `PeerConfig.addresses` is now `serde(default)` and may be empty
when `via_nostr: true`; validation requires at least one of the
two to be present per peer, and the error message names the
peer's npub.
- `UdpConfig.advertise_on_nostr: Option<bool>` and
`UdpConfig.public: Option<bool>` — UDP transports can be
advertised either as direct `host:port` (public = true) or as the
`addr: "nat"` sentinel that triggers rendezvous on the peer side.
- `TcpConfig.advertise_on_nostr` and `TorConfig.advertise_on_nostr`
— TCP and Tor onion endpoints can be advertised as directly
reachable.
- A reserved peer address `transport: udp, addr: "nat"` parses without
special-casing in YAML and routes through the bootstrap runtime.
Cross-field validation (`Config::validate`, called from `Node::new`
and `Node::with_identity`):
- Any transport with `advertise_on_nostr = true` requires
`node.discovery.nostr.enabled = true`.
- Any peer with `via_nostr = true` requires
`node.discovery.nostr.enabled = true`.
- A non-public UDP advert (`advertise_on_nostr = true`,
`public = false` — i.e. `udp:nat`) additionally requires at least
one `dm_relay` and at least one `stun_server`.
Surfaced as `ConfigError::Validation`.
== Node integration
`src/node/lifecycle.rs` is the main integration point.
- At node start (after transports are up, before TUN), if Nostr is
enabled and the feature is compiled in, `NostrDiscovery::start` is
invoked, the initial local overlay advert is built from the live
transport set and published, and the runtime handle is stored.
- The rx tick loop calls `poll_nostr_discovery` (feature-gated both
at method definition and call site), which refreshes the local
advert, drains bootstrap events, adopts established traversals,
schedules retries for failed traversals, and — under `policy:
open` — enqueues outbound retries for non-configured peers
visible in the advert cache, bounded by
`open_discovery_max_pending` and the remaining outbound slots.
- Outbound peer dialing is refactored to `try_peer_addresses`, which
first exhausts the static address list in priority order and only
then appends advert-derived fallback addresses; both lists run
through the same `attempt_peer_address_list` code path. The
`udp:nat` sentinel address triggers `NostrDiscovery::request_connect`
for the peer instead of a direct dial and returns `Ok(())`.
- `build_overlay_advert` walks operational transports, consults
per-instance `UdpConfig` / `TcpConfig` / `TorConfig` (matching by
optional transport instance name), and emits an `OverlayAdvert`
including `signalRelays` and `stunServers` when any UDP endpoint
is advertised as NAT.
- `adopt_established_traversal` is the bootstrap handoff API:
allocates a new `TransportId`, constructs a `UdpTransport` with
the user-supplied (or default) `UdpConfig`, calls the new
`adopt_socket_async` to reuse the punched socket verbatim,
registers the transport in the normal transport map, records it
in `bootstrap_transports`, and calls `initiate_connection` so the
normal handshake path runs. On failure, the transport is stopped
and removed cleanly and the set membership is rolled back.
- On clean shutdown, `NostrDiscovery::shutdown` is awaited so
background tasks stop before transports are torn down. (The
advert is not explicitly retracted; NIP-40 expiration plus the
next refresh from any live publisher supersedes it.)
New `Node` fields:
- `nostr_discovery: Option<Arc<NostrDiscovery>>` (feature-gated).
- `bootstrap_transports: HashSet<TransportId>` — per-peer UDP
transports adopted from NAT traversal, cleaned up via
`cleanup_bootstrap_transport_if_unused` whenever the link,
connection, peer, or pending-connect referencing them is removed.
Retry and error surface:
- `RetryState.expires_at_ms: Option<u64>` — optional absolute expiry
for a retry entry. `pump_retries` drops expired entries with an
info log. Used for open-discovery retries, which expire at two
times the advert TTL.
- New `NodeError::BootstrapHandoff(String)` returned from
`adopt_established_traversal` when the underlying transport
adoption fails or local address discovery fails.
- New `ConfigError::Validation(String)`.
- A small refactor extracts `Node::now_ms()` and reuses it across
lifecycle, rx-loop tick, and timeout bookkeeping.
== UDP transport
`src/transport/udp/`:
- `UdpRawSocket::adopt(std::net::UdpSocket, recv_buf, send_buf)`:
adopts an externally bound socket, makes it non-blocking, applies
the configured buffer sizes (warning if the kernel clamps), and
reports the resulting local address. Preserves the NAT mapping —
no rebind.
- `UdpTransport::adopt_socket_async(std::net::UdpSocket)`: the
`start_async` analogue for an already-bound socket, wiring the
async socket and recv task exactly as the fresh-bind path would.
- `Drop` impl for `UdpTransport`: if a transport is dropped while
still holding a recv task or socket (for example on error
teardown), aborts the task, clears the socket, and emits a debug
log so the cleanup is visible in tracing rather than silent.
== Logging and observability
Default `EnvFilter` demotes third-party relay-pool DEBUG output to
TRACE-only: `nostr_relay_pool`, `nostr_sdk`, and `nostr` are pinned
at INFO when our level is anything below TRACE, and at TRACE when
our level is TRACE — so the raw frames are still reachable when
explicitly asked for. RUST_LOG continues to override completely.
Concise one-line DEBUG events are emitted at the meaningful points
in the discovery / hole-punch sequence:
- `advert: published` (event id, relay count, endpoints, ttl)
- `advert: peer cached` (notify-loop ingress for non-self)
- `advert: resolved` (cache hit / relay fetch outcome)
- `traversal: initiator starting`
- `traversal: initiator STUN observed` (reflexive, local count)
- `traversal: offer sent` (session id, relay count, event id)
- `traversal: answer received` (accepted, reflexive, local)
- `traversal: initiator punch succeeded` (remote addr)
- `traversal: offer received` (responder side)
- `traversal: responder STUN observed`
- `traversal: answer sent`
- `traversal: responder punch succeeded`
Npubs are shortened to `npub1<4>..<4>` and event/session ids to
their first 8 hex characters.
Other operator-facing logs:
- `UdpTransport` adoption and drop paths log at info / debug.
- `adopt_established_traversal` logs at debug on entry and info on
successful return, tagged with peer npub, session id, transport
id, and both socket endpoints, so the bootstrap handoff is
traceable end-to-end alongside the `UdpTransport::drop` log.
- `cleanup_bootstrap_transport_if_unused` logs at debug when the
reference-count check drops an adopted transport.
- `connect_peer` tags its entry `debug!` with `peer_npub` so
downstream STUN, punch, and handshake logs for the same peer
correlate for operators.
- Advert-cache and seen-sessions overflow evictions log at debug so
mis-sized caps are visible under ambient relay volume.
- Gift-wrap unwrap failures on `SIGNAL_KIND` events log at trace
(hot path: fires for every unrelated signal event on the same
relay).
- Traversal-offer handler failures log at debug. Expected conditions
such as punch timeout on symmetric NAT are covered there; real
problems are reported upstream via `BootstrapEvent::Failed`.
- Inbound-offer rate-limit messages name the governing config field
(`max_concurrent_incoming_offers`) and state that the offer was
rate-limited rather than failing.
== Tests
- 18 new unit tests in `src/discovery/nostr/tests.rs` covering advert
encoding, signal envelope round-trip, STUN parsing, punch-packet
codec, and replay-window enforcement. Run under the
`nostr-discovery` feature.
- Config-validation tests in `src/config/mod.rs` covering the three
cross-field invariants and YAML parsing of the full
`node.discovery.nostr` block plus `peers[].via_nostr`, empty
`addresses` with `via_nostr: true`, and a `udp: nat` address.
- `src/node/tests/bootstrap.rs` integration tests that drive a
synthetic traversal (bound UDP socket pair + synthetic peer
identity) through `adopt_established_traversal` and assert the
Noise handshake completes over the adopted socket.
- Punch-planner tests assert reflexive-before-LAN ordering and that
same-LAN scenarios still include the LAN target in the plan.
- `testing/nat/` Docker NAT lab harness:
- Local `strfry` relay, local STUN responder, and one or two
router containers performing `iptables` NAT.
- Node LAN interfaces are provisioned with explicit `veth` pairs
injected into the node and router namespaces so every packet
traverses the router namespace (plain Docker bridges are not
used for the LAN).
- `cone` scenario: both peers behind full-cone-emulation NAT
(SNAT with source-port preservation, inbound DNAT back to the
single LAN host regardless of remote source); asserts UDP
traversal succeeds and link remote addresses are on the router
WAN subnet.
- `symmetric` scenario: `MASQUERADE --random-fully`; asserts UDP
traversal fails and TCP fallback converges over router-
published WAN addresses.
- `lan` scenario: both peers share a LAN subnet; asserts LAN
addresses are preferred over reflexive ones.
- Cleanup tears down all profile-gated services
(`--profile cone --profile symmetric --profile lan`) so no
orphan containers survive a run.
- `testing/scripts/build.sh` builds the Docker test image with
`--features "tui nostr-discovery"` by default so NAT-harness
binaries include bootstrap support.
== CI
- Linux release build and nextest unit-test job both use
`--features "gateway nostr-discovery"` so the feature-gated code
and its unit tests compile and run in CI.
- Three new integration matrix entries (`nat-cone`, `nat-symmetric`,
`nat-lan`) invoke `testing/nat/scripts/nat-test.sh`, collect
`docker compose logs` on failure, and always stop containers.
== Packaging and operations
- `packaging/common/fips.yaml` ships a fully commented
`node.discovery.nostr.*` block, plus documented
`advertise_on_nostr` / `public` examples under the UDP transport,
an `advertise_on_nostr` example under TCP, and a `via_nostr: true`
example under the static peer section with both a direct
`host:port` UDP address and a `udp: nat` fallback.
- `.github/workflows/package-openwrt.yml`: NIP-94 release event
publishes target the new default relay set.
== Documentation
- `README.md`: overlay discovery + NAT traversal moved from
"Near-term priorities" into "What works today".
- `docs/design/fips-intro.md`: rewrites the paragraphs that
previously described Nostr discovery and NAT traversal as future
work; describes the shipped mechanism and the feature gate.
- `docs/design/fips-transport-layer.md`: drops the "(future
direction)" qualifier from the Nostr Relay Discovery section,
expands with the `udp:nat` advertisement and bootstrap handoff
description, and updates the Current State callout.
- `docs/design/fips-mesh-layer.md`: notes that mid-session NAT
rebinding (roaming) and initial NAT traversal (Nostr path) are
distinct mechanisms.
- `docs/design/fips-configuration.md`: documents the full
`node.discovery.nostr.*` surface, including the three resource
caps and `share_local_candidates`.
- `docs/design/fips-nostr-discovery.md`: design and configuration
reference for the shipped mechanism, including the empty-
`addresses`-with-`via_nostr` shorthand.
- `docs/proposals/nostr-udp-hole-punch-protocol.md`: adds an
Implemented status callout, clarifies that the punch socket is
per-peer and per-attempt rather than shared with the application
listener, aligns field names with the shipped JSON
(`sessionId`, `issuedAt` / `expiresAt`, `reflexiveAddress`,
`localAddresses`, `stunServer`), sets the `d`-tag to
`fips-overlay-v1`, names the kind as 37195, and notes that
advertised STUN entries are informational.
- `docs/proposals/README.md`: adds a Status column and marks the
hole-punching proposal Implemented.
- `CHANGELOG.md`: Unreleased > Added entry covering the discovery
path, STUN/punch path, configuration surface, and Docker NAT lab.
Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
|
||
|
|
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. |
||
|
|
c83e14ac97 |
Switch std::atomic to portable_atomic for mips support (#62)
Co-authored-by: andrewheadricke <andrewheadricke> |
||
|
|
774e33fd27 |
Add Windows platform support (#45)
Gate platform-specific code behind cfg attributes and add full Windows
support: TUN device via wintun, TCP control socket on localhost:21210,
Windows Service lifecycle (--install-service/--uninstall-service/--service),
CI build and test matrix, and packaging with ZIP builder and PowerShell
service management scripts.
Key changes:
- Cargo.toml: move tun/libc/rtnetlink behind cfg(unix); add wintun and
windows-service dependencies for Windows
- upper/tun.rs: wintun-based TUN implementation with netsh configuration
for IPv6 address, MTU, and fd00::/8 routing
- control/mod.rs: split into unix_impl/windows_impl; Windows uses TCP on
localhost:21210 with shared connection handler
- bin/fips.rs: refactor main() into run_daemon() accepting a shutdown
signal; add Windows Service support via windows-service crate
- transport/udp/socket.rs: platform-gated modules; Windows uses
tokio::net::UdpSocket (kernel drop count unavailable, returns 0)
- transport/ethernet: gate to cfg(unix); add Windows stub types
- config: platform-conditional default paths (socket, hosts) for Windows
- CI: add windows-latest to build matrix and test-windows job with
cargo-nextest
- packaging/windows: build-zip.ps1, install-service.ps1,
uninstall-service.ps1, and package-windows.yml workflow
- README/docs: Windows build instructions, service management, and
control socket platform differences
Linux and macOS behavior is unchanged.
|
||
|
|
e9da598f8a | Apply rustfmt to master-only code | ||
|
|
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 |
||
|
|
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. |
||
|
|
e693f4fb7e |
Add macOS support, fix bloom filter routing and MMP intervals
macOS platform: - Platform-native TUN interface management with shutdown pipe - Raw Ethernet transport with macOS socket backend (socket_macos.rs) - EthernetTransport and TransportHandle::Ethernet ungated from Linux-only - macOS .pkg packaging (build-pkg.sh, launchd plist, uninstall script) - CI: macOS build and unit test jobs; x86_64 cross-compiled from macos-latest via rustup target add x86_64-apple-darwin Gateway feature flag: - New opt-in `gateway` Cargo feature activates optional `rustables` dep - `pub mod gateway` and `Config.gateway` gated behind the feature so macOS builds never pull in Linux-only nftables bindings - `fips-gateway` bin has `required-features = ["gateway"]` - All Linux/OpenWrt/AUR packaging passes `--features gateway` CI / packaging: - package-linux, package-macos, package-openwrt now trigger on push to master/maint/next and on pull requests; release uploads remain tag-gated - Bloom filter routing fix: fall through to tree routing when no candidate is strictly closer - MMP intervals: raise MIN to 1s / MAX to 5s with 5-sample cold-start phase |
||
|
|
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. |
||
|
|
51119347c3 |
Fix rekey msg1 rejected on non-accepting transports (#49)
The accept_connections gate in handle_msg1() was at the top of the function, dropping all inbound msg1 packets on transports that don't accept new connections (e.g. UDP holepunch). This blocked rekey handshakes on established links, causing repeated "dual rekey initiation" log floods. Move the gate below the existing-peer classification so it only blocks truly new inbound handshakes from unknown addresses. Rekey and restart msg1s for established peers are now processed normally. Fixes #47 PR #49 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
56d39f223b |
Add ECN congestion signaling and transport congestion detection
Implement hop-by-hop ECN congestion signaling through the FMP layer, transport-level congestion detection via kernel drop counters, and chaos harness integration for end-to-end validation. FMP/session ECN plumbing: - Thread ce_flag parsed at link layer through dispatch_link_message, handle_session_datagram, handle_session_payload, and handle_encrypted_session_msg to session delivery - Replace hardcoded false in session-layer record_recv() with actual ce_flag, activating ecn_ce_count tracking in session MMP ECN congestion detection and CE relay: - Add EcnConfig (node.ecn.*) with configurable loss_threshold (5%) and etx_threshold (3.0) for transit congestion detection - Add send_encrypted_link_message_with_ce() that ORs FLAG_CE into FMP header flags; original method delegates with ce_flag=false - Compute outgoing_ce = incoming_ce || local congestion on next-hop link, enabling hop-by-hop CE relay through transit nodes IPv6 ECN-CE marking: - Mark ECN-CE (0b11) in IPv6 Traffic Class on received DataPackets before TUN delivery when FMP CE flag is set - Only marks ECN-capable packets (ECT(0)/ECT(1)); Not-ECT packets unchanged per RFC 3168 Transport congestion abstraction and UDP kernel drop detection: - Add TransportCongestion struct to transport layer for transport- agnostic local congestion indicators - Replace tokio::UdpSocket with AsyncFd<socket2::Socket> using libc::recvmsg() with ancillary data parsing - Enable SO_RXQ_OVFL for kernel receive buffer drop counter on every packet, wiring up previously-stubbed UdpStats.kernel_drops - Add TransportDropState for per-transport delta tracking with 1s tick sampling via sample_transport_congestion() - Extend detect_congestion() with transport kernel drop check alongside MMP loss metrics Congestion monitoring and control: - Add CongestionStats (ce_forwarded, ce_received, congestion_detected, kernel_drop_events) to NodeStats with snapshot serialization - Wire counters into forwarding path, session handler, and transport drop sampling with rate-limited warn logging (5s interval) - Expose congestion data in show_routing control query and ecn_ce_count in show_mmp peer entries - Add congestion counters to fipstop routing tab in two-column layout Chaos harness integration: - Add query_routing(), query_transports(), snapshot_all_congestion() to chaos control module - Add congestion/kernel-drop log analysis in logs module - Add congestion-stress scenario: 10-node tree, 1 Mbps bandwidth, 5-10% netem loss, heavy iperf3 traffic - Add IngressConfig for tc ingress policing with per-peer policer filters simulating upstream bandwidth bottlenecks - Add iperf3 JSON result capture to traffic manager for throughput measurement across scenarios - Add ECN A/B test scenarios (ecn-ab-on/off.yaml) with ingress policing and comparison script - Enable TCP ECN negotiation (tcp_ecn=1 sysctl) in container entrypoint for end-to-end CE propagation Tests: - 10 ECN unit/integration tests: mark_ipv6_ecn_ce variants, CE relay chain (3-node propagation), EcnConfig serde roundtrip - 3 transport drop congestion detection unit tests Documentation: - Update fips-mesh-layer.md: replace outdated CE Echo stub with full ECN Congestion Signaling section covering detection logic, CE relay, IPv6 marking, session tracking, and monitoring counters - Update fips-configuration.md: add node.ecn.* parameter table and ecn block in complete reference YAML - Update fips-transport-layer.md: add Congestion Reporting section with TransportCongestion struct, congestion() trait method, and per-transport status; document AsyncFd/recvmsg/SO_RXQ_OVFL in UDP - Update chaos README: add congestion/ECN scenario docs, ingress traffic control, and iperf3 JSON capture sections - Update README.md: add ECN to features list and "What works today"; update transport and tooling entries |
||
|
|
6be05f0a0a |
Add FMP version check to TCP stream reader
Non-FIPS clients (e.g., TLS connections) hitting the TCP listen port produce misleading "unknown FMP phase" errors because the stream reader checked phase before version. A TLS ClientHello (byte 0x16) parsed as version=1, phase=6. Add UnknownVersion error variant and check the version nibble before phase dispatch, so non-FIPS connections now report "unknown FMP version: 1" instead of "unknown FMP phase: 0x06". |
||
|
|
48464e63c1 |
Fix Ethernet AEAD decryption failures caused by minimum-frame padding 🤦
Ethernet requires a minimum 46-byte payload (60-byte frame minus 14-byte header). NICs/drivers pad shorter frames with zeros. The Ethernet transport had no length field, so the receiver included padding bytes in the ciphertext, causing AEAD (ChaCha20-Poly1305) authentication tag mismatch on small frames like heartbeats (39 bytes with prefix, padded to 46). Add a 2-byte little-endian payload length field after the frame type byte. Wire format changes from [type:1][payload] to [type:1][length:2 LE][payload]. The receiver uses the length field to extract exactly the right number of bytes, ignoring any NIC padding. Frame overhead increases from 1 to 3 bytes, effective MTU adjusted accordingly. This is a wire-format breaking change requiring simultaneous upgrade of all Ethernet peers. |
||
|
|
71a5c68fa9 |
Implement comprehensive node and transport statistics
Add 71 new counters (84 values) across three categories: Node statistics (NodeStats, plain u64 — single handler context): - Forwarding: 9 counters x (packets + bytes) = 18 values. Covers received, decode_error, ttl_exhausted, delivered, forwarded, drop_no_route, drop_mtu_exceeded, drop_send_error, originated. - Discovery: 17 counters (packets only). Request path: received, decode_error, duplicate, already_visited, target_is_us, forwarded, ttl_exhausted, initiated, deduplicated. Response path: received, decode_error, forwarded, identity_miss, proof_failed, accepted, timed_out. - Error signals: 3 counters — coords_required, path_broken, mtu_exceeded. - Spanning tree: 16 counters. Inbound announce handling (received through accepted, parent switch, loop detection, ancestry change), outbound (sent, rate limited, send failed), cumulative events (parent switches/losses, flap dampening). - Bloom filter: 10 counters. Inbound (received through accepted), outbound (sent, debounce suppressed, send failed). Transport statistics (AtomicU64 + Arc — shared with spawned tasks): - UDP (6 counters, 8 values): packets/bytes sent/recv, send_errors, recv_errors, mtu_exceeded, kernel_drops (stub for SO_MEMINFO). - TCP (10 counters, 12 values): packets/bytes sent/recv, send_errors, recv_errors, mtu_exceeded, plus connection lifecycle counters (established, accepted, rejected, timeouts, refused). Control socket integration: - show_routing: forwarding, discovery, error signal stats - show_tree: spanning tree stats + per-peer bloom metrics (estimated_count, set_bits, fill_ratio) and coordinate paths - show_bloom: bloom filter stats + per-peer snapshots - show_transports: per-transport stats snapshots Also refactor UDP transport from flat files (udp.rs + udp_stats.rs) into directory module (udp/mod.rs + udp/stats.rs) matching TCP structure, and fix pre-existing clippy warnings in tree/tests.rs. |
||
|
|
32054271f5 |
Fix clippy warnings in test code
Replace assert_eq! with literal bool, collapse nested if-let, use iterators instead of indexed loops, use slice instead of vec. |
||
|
|
ec64a0dce1 |
Add TCP transport implementation and test harness support
Implement TCP transport for FIPS enabling firewall traversal and serving as the foundation for future Tor transport. This is the first connection-oriented transport in the system. Key design decisions: - FMP header-based framing: reuses existing 4-byte FMP common prefix for packet boundary recovery with zero framing overhead - Session survives TCP reconnection: Noise/MMP/FSP state bound to npub, not TCP connection; MMP liveness is sole authority for peer death - Connect-on-send: fresh connection on first send, transparent reconnect - close_connection() trait method for cross-connection deduplication cleanup New transport files: - src/transport/tcp/mod.rs: TcpTransport, connection pool, accept loop - src/transport/tcp/stream.rs: FMP-aware stream reader (shared with Tor) Modified: transport trait (close_connection), TcpConfig, TransportHandle match arms, create_transports(), initiate_connection() for connection- oriented links, cross-connection tie-breaker cleanup, design docs. Tree announce loop and TCP stability fixes: - Preserve tree announce rate-limit state across reconnection: carry forward last_tree_announce_sent_ms when a peer reconnects so the rate-limit window isn't reset to zero - Drop oversize TCP packets at sender: pre-send MTU check returns MtuExceeded instead of writing to the stream, preventing receiver-side connection teardown and reset-reconnect cycles Chaos harness: - TCP transport support: tcp_edges/has_tcp/tcp_peers in SimTopology, transport-aware config_gen with per-edge transport type, TCP port 443, pure-TCP node support - Include all non-Ethernet edges in directed_outbound() - Fix netem/links log messages to say "IP-based" instead of "UDP" - Add tcp-chain, tcp-only, and tcp-mesh scenario files Static harness: - Transport-aware config generation (get_default_transport, transport_port) - TCP transport injection via Python post-processing - Add tcp-chain topology and docker-compose profile |