Commit Graph
16 Commits
Author SHA1 Message Date
ArjenandJohnathan Corgan cf62cff5f4 feat: Android-ready core: target_os gating and app-owned TUN seam
Make the FIPS core build and run as an embedded Android library. The host
app owns the TUN (e.g. an Android VpnService) and FIPS performs no
system-TUN or CAP_NET_ADMIN operations.

Squashed from the following changes:

- gate desktop transports/TUN by target_os, not features: a plain
  `cargo build` now compiles for every target with no flags. Ethernet (raw
  AF_PACKET / BPF) is gated to linux/macos, so Android (target_os =
  "android", not "linux") self-excludes it as Windows already did; real
  system-TUN ops are gated per linux/macos and Android gets a no-op stub;
  the ipi6_ifindex cast handles it being i32 on Android vs u32 on macOS. No
  Cargo features are introduced; desktop builds are unchanged.

- app-owned TUN seam: Node::enable_app_owned_tun() lets an embedder that
  owns the TUN fd exchange IPv6 packet bytes with FIPS over channels
  instead of FIPS creating a system TUN device. It returns (app_outbound_tx,
  app_inbound_rx): the embedder pushes packets read from its fd into the
  outbound sender (app -> mesh) and pulls packets destined for its fd from
  the inbound receiver (mesh -> app). start() gates system-TUN creation on
  tun_tx being unset, so with the channels pre-installed it skips device
  creation and does no system-TUN ops; both directions reuse the existing
  inbound-shim and run_rx_loop wiring. Packets entering via app_outbound_tx
  bypass handle_tun_packet, so the embedder must push only fd00::/8-destined
  packets and clamp TCP MSS on outbound SYNs; the rustdoc and the
  IPv6-adapter design doc spell this out.

- keep the android target warning-clean so the cross-compile check passes
  clippy -D warnings.

- add an Android cross-compile CI check: cross-compile the library for
  aarch64-linux-android via cargo-ndk and run clippy -D warnings. Android
  ships as an embedded library (the host app owns the TUN), so there is no
  daemon binary to package; this is a check job, not a packaging one.

- docs: list Android as a supported platform.

Tests: app_owned_tun_seam_wires_channels covers the channel round-trip and
the Active state; start_skips_system_tun_when_app_owned runs start() and
asserts no named system device is created.
2026-07-17 18:25:55 +00:00
ArjenandGitHub 225fab29ab fix(tun): complete L4 checksum on hairpinned self-traffic (macOS) (#117)
Self-addressed TCP/UDP connections to a node's own <npub>.fips address
half-opened and hung on macOS. macOS routes self-traffic as loopback (a
LOCAL route via lo0), which defers the transport TX checksum, but the
point-to-point utun then egresses the packet into the daemon with only
the pseudo-header partial checksum present. The hairpin path added in
9a9e90a re-injected these verbatim, so the local stack dropped every
segment whose checksum MSS clamping didn't happen to rewrite: the
SYN/SYN-ACK got through (clamping recomputes them) but the bare ACK,
data, and FIN were dropped for a bad checksum, leaving the listener
stuck in SYN_RCVD.

Recompute the TCP/UDP checksum for self-addressed packets on the hairpin
path before re-injection, completing the self-delivery 9a9e90a started
(which only covered ICMP and the TCP handshake). Linux is unaffected: it
loops self-traffic via lo before the TUN, so the hairpin branch never
fires and checksums are already valid.

Confirmed on macOS: a self-connect that previously timed out now
completes in ~9ms with payload delivered.
2026-06-18 08:36:57 -07:00
ArjenandJohnathan Corgan 9a9e90a32c fix(tun): loop back self-addressed packets for local delivery
A packet destined for our own mesh address reached the TUN reader on
macOS (utun egresses self-traffic into the daemon despite the lo0 host
route) and was pushed onto the mesh outbound path, where it was dropped
for lack of a session/route to self. Hairpin self-addressed packets back
to the TUN writer instead, so ping6 and connections to our own
<npub>.fips address are delivered locally.

On Linux the kernel already loops self-traffic via `lo` before it reaches
the TUN, so the branch never fires there; the check is kept unconditional
as a daemon-level delivery invariant and to keep it covered by Linux CI.
2026-06-17 14:32:17 +00:00
Martti MalmiandJohnathan Corgan 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.
2026-05-17 17:55:52 +00:00
Johnathan Corgan 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.
2026-05-03 21:02:51 +00:00
Johnathan Corgan 953137ede7 Plumb path_mtu_lookup into Windows run_tun_reader
The B3 path_mtu_lookup plumbing landed without updating the
windows_tun::run_tun_reader signature or its inner handle_tun_packet
call, breaking the Windows build. Linux/macOS variants and
TunWriter (Windows) were already plumbed; this brings the Windows
reader into line.

No behavioral change on any platform.
2026-05-02 18:23:17 +00:00
Johnathan Corgan ae607431eb Per-destination TCP MSS clamping at the TUN boundary
Adds source-side TCP MSS clamping informed by per-destination path
MTU learned via discovery, with a conservative IPv6-minimum-derived
ceiling for cold flows where discovery has not yet completed. Closes
the multi-hop default-config TCP wedges observed in production where
a sender's local-floor MSS exceeds what some intermediate forwarder
hop is willing to carry: silent drops, no PTB feedback through the
userspace TUN to the kernel TCP stack, retransmits at the same too-
large MSS, application connection times out.

## Architecture

A new `Arc<RwLock<HashMap<FipsAddress, u16>>>` field
`path_mtu_lookup` on Node mirrors the per-destination path MTU in a
form accessible from sync TUN reader/writer threads. A new
`per_flow_max_mss` helper in `src/upper/tun.rs` reads the lookup at
SYN-clamp time and returns the appropriate ceiling for the flow.

Three write sites populate `path_mtu_lookup`:

1. **Discovery originator branch** of `handle_lookup_response`:
   the path MTU bottleneck accumulated through the reverse path
   lands here when a LookupResponse arrives at the originator.
   Same value also lands in `coord_cache` per the existing
   `insert_with_path_mtu` API.

2. **FMP peer-promotion seed** (`seed_path_mtu_for_link_peer`):
   when an FMP link-layer peer is promoted to active, the local
   outgoing-link MTU on the peer's transport seeds the lookup.
   Tighter existing values (learned via discovery) are preserved;
   the seed only writes when no entry exists or the existing
   value is looser than the link MTU. Without this seed,
   directly-configured peers (auto_connect / static peer config)
   would leave `path_mtu_lookup` empty for their FipsAddress
   because the FSP session establishes without ever issuing a
   LookupRequest.

3. **Target-edge fold at `send_lookup_response`**: when a node is
   the discovery target, it folds its own outgoing-link MTU to
   the response's next-hop into `path_mtu` before sending.
   Without this fold, the response leaves the target with
   `path_mtu = u16::MAX` and only intermediate transits min-fold;
   the target's first reverse-path hop is never represented in
   the bottleneck calculation. Refactored the existing transit-
   side min-fold into a shared `apply_outgoing_link_mtu_to_response`
   helper called from both sites.

## Read-side: per_flow_max_mss

Two TUN call sites consume the lookup:

- Outbound `handle_tun_packet` clamps SYN MSS using packet[24..40]
  (IPv6 destination) as the lookup key.
- Inbound `TunWriter::run` clamps SYN-ACK MSS using packet[8..24]
  (IPv6 source).

When the lookup contains a learned value, the helper computes
`min(global_max_mss, effective_ipv6_mtu(path_mtu) - 60)` where 60
is IPv6 (40) + TCP (20) headers and `effective_ipv6_mtu` accounts
for the FIPS encapsulation overhead.

When the lookup is empty for a destination — the cold-flow case —
the helper returns `min(global_max_mss, IPv6-minimum-derived
ceiling)`. RFC 8200 mandates every IPv6 path accept ≥1280-byte
packets, so the IPv6-minimum-derived MSS (1280 - 77 - 60 = 1143)
fits any compliant path. Without this conservative ceiling, the
first SYN to a destination with no learned path MTU exits the TUN
at the kernel-natural MSS (TUN MTU - 60), and the application
connection wedges silently before discovery completes for a
corrected second SYN to fire. The fix is provably safe: the
ceiling is taken with `min` against the local global so operators
with even tighter local floors are never loosened upward.

Subsequent flows pick up the actual learned per-destination value
once discovery (or the FMP-promotion seed for direct peers)
populates the lookup.

## Diagnostic logging

All write and read sites emit instrumentation suitable for
operators bisecting a wedged path:

- `debug!` log on every `path_mtu_lookup` write (discovery
  originator path and FMP-promotion seed path), showing the
  FipsAddress, written value, prior value, and post-write map
  size. `warn!` on poisoned-lock failure path.
- `trace!` log per `per_flow_max_mss` call covering every
  fall-through branch (wrong addr_bytes length, non-fd::/8
  prefix, lookup poisoned, no entry for destination, empty-lookup
  conservative ceiling) and the success path. trace level filters
  out under normal log settings; capture with
  `RUST_LOG=info,fips::node::handlers::discovery=debug,fips::upper::tun=trace`.

## Tests

15 new unit tests across 3 files:

- `per_flow_max_mss` (8 tests in `src/upper/tun.rs::tests`):
  empty-lookup conservative ceiling, empty-lookup global-smaller
  floor, learned-value-overrides-conservative, per-destination
  smaller, per-destination larger capped by global, non-fips
  addr, short addr slice, per-destination independence.
- `seed_path_mtu_for_link_peer` (4 tests in
  `src/node/tests/unit.rs`): seed when empty, keep tighter
  existing, tighten looser existing, no-op for unknown
  transport.
- Discovery integration (3 tests in `src/node/tests/discovery.rs`):
  apply_outgoing_link_mtu_to_response on unknown peer no-op,
  two-node target-edge fold (path_mtu reflects target-edge link),
  three-node chain transit min-fold (existing test, updated for
  target-edge inclusion).

Two pre-existing discovery tests had assertions updated to
account for the target-edge fold:

- `test_response_path_mtu_two_node`: previously asserted
  `u16::MAX` (no transit to min-fold); now asserts 1280 (the
  test transport MTU, folded in by send_lookup_response).
- `test_response_path_mtu_four_node_chain`: previously asserted
  1350 (transit MTUs only); now asserts 1280 (target-edge MTU
  is the bottleneck).
- `test_transit_forwards_when_mtu_sufficient`: previously
  asserted 1400 (transit MTU only); now asserts 1280 (target-
  edge MTU is the bottleneck).

## Verification

Local CI on this commit: 29/29 suites pass, 1105 lib tests pass,
clippy --all-targets --all-features -D warnings clean, cargo fmt
clean. Production deploy verified via trace capture across the
managed fleet: cold-flow conservative ceiling branch fires on
first SYN, learned-lookup branch takes over once discovery
completes, both behaviors observable end-to-end at the SYN MSS
on the wire.

No wire-format change. No config-format change.
2026-05-02 17:37:07 +00:00
OceanSlimandGitHub 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.
2026-04-11 18:31:48 +01:00
Johnathan Corgan e9da598f8a Apply rustfmt to master-only code 2026-04-10 08:47:02 +00:00
Origami74 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
2026-04-09 20:03:42 +01: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 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 58664c7c77 Update dependencies: rand 0.10, rtnetlink 0.20, tun 0.8, and others
Bump rand (0.8→0.10), rtnetlink (0.14→0.20), tun (0.7→0.8),
simple-dns (0.9→0.11), socket2 (0.5→0.6), and criterion (0.5→0.8).

Migrate all rand call sites: thread_rng()→rng(), gen()→random(),
gen_range()→random_range(), RngCore→Rng trait. Work around secp256k1
0.30 requiring rand 0.8 by generating random bytes directly and
constructing SecretKey from slice.

Migrate rtnetlink to builder-based API: LinkSetRequest replaced with
LinkUnspec builder + change(), RouteAddRequest replaced with
RouteMessageBuilder.

Remove bloom benchmark (criterion 0.8 incompatible with old harness
config).
2026-02-24 18:02:06 +00:00
Johnathan Corgan ab7a4bac29 Logging level overhaul: reduce verbosity at info and debug levels
Per-packet happy-path events (UDP send/receive, TUN I/O, MMP report
processing, TreeAnnounce/FilterAnnounce sent, RTT samples) moved from
debug to trace. Periodic maintenance and retry scheduling moved from
info to debug. Session state changes (established, initiated, torn
down) and transport stop promoted from debug to info. MMP report send
failures demoted from warn to debug (normal under churn).
2026-02-19 04:15:08 +00:00
Origami74andJohnathan Corgan 852f561fa0 feat: implement ICMP Packet Too Big and TCP MSS clamping for MTU handling
Add dual-approach MTU handling to prevent TCP connections from hanging
when packets exceed the transport MTU after FIPS encapsulation.

ICMPv6 Packet Too Big:
- Generate RFC 4443 PTB messages for oversized packets at TUN outbound
- Inject back via TUN for local delivery to the application
- Per-source rate limiting (100ms interval, 10s entry expiry)
- MTU check in handle_tun_outbound before session encapsulation

TCP MSS Clamping:
- Intercept SYN packets in run_tun_reader() (outbound)
- Intercept SYN-ACK packets in TunWriter (inbound)
- Clamp MSS option to fit within effective MTU (transport - 127 overhead)
- Recalculate TCP checksum after modification

Code organization:
- ICMP, TCP MSS, and rate limiter modules in upper/ alongside existing
  protocol-specific packet handling (dns.rs, tun.rs)
- Shared FIPS_OVERHEAD constant (127 bytes) and effective_ipv6_mtu()
  function in upper/icmp.rs
- Node::effective_ipv6_mtu() delegates to the shared function
- run_tun_reader() accepts actual transport MTU from config

Example config corrections:
- UDP transport MTU set to 1472 across all configs (correct max UDP
  payload for standard Ethernet: 1500 - 20 IPv4 - 8 UDP)
- Startup logging of effective MTU and max MSS values
2026-02-16 21:33:21 +00:00
Johnathan Corgan d71e48b0f2 Module reorganization, identity test coverage, design doc corrections
Module reorganization:

- Split identity.rs (930 lines) into identity/ directory module:
  mod.rs, node_addr.rs, address.rs, peer.rs, local.rs, auth.rs,
  encoding.rs, tests.rs — following established bloom/, tree/, noise/
  pattern

- Group TUN, DNS, and ICMPv6 into upper/ module as the IPv6 adaptation
  layer: move tun.rs, icmp.rs, node/dns.rs into upper/

Identity test coverage (28 new tests, 52 total):

- Encoding error paths: invalid npub/nsec length, bad hex input
- NodeAddr: Debug, Display, as_slice, AsRef, Hash
- FipsAddress: from_slice, From trait, Debug, Display, Eq+Hash
- PeerIdentity: from_pubkey_full, pubkey_full parity paths, Debug
- Identity: keypair, pubkey_full, Debug
- AuthChallenge: from_bytes

Design doc corrections (fips-software-architecture.md):

- Identity struct: npub+nsec fields → keypair: Keypair with accessors
- Node struct: TunInterface → TunState, Transport → TransportHandle,
  Peer → PeerSlot
- Peer section: monolithic Peer → two-phase PeerSlot (PeerConnection +
  ActivePeer) with HandshakeState/ConnectivityState
- ActivePeer: npub → identity: PeerIdentity, ancestry Vec → Option,
  declaration/inbound_filter wrapped in Option
- BloomState: add 4 missing fields, fix update_debounce type
- DiscoveredPeer: field name and type corrections
2026-02-15 17:11:58 +00:00