- Correct the transport matrix: UDP, TCP, and Tor work on Windows
(previously shown as unsupported). Add an OpenWrt column with BLE
disabled due to missing libdbus on the target.
- Mention the `.fips` DNS resolver and outbound LAN gateway in the
Features list, and add the gateway bullet under What works today.
- Reframe the Linux DNS resolver setup: the `.deb` package now
auto-configures the available backend; the manual resolvectl
snippet is shown for tarball and manual installs.
- Expand the Examples section to list all three example deployments
(Nostr relay sidecar, K8s sidecar, macOS WireGuard sidecar) rather
than only the macOS one.
- Refresh Project Structure to include the `fips-gateway` binary,
the full packaging list (macOS .pkg, Windows ZIP, OpenWrt ipk, AUR
in addition to Debian and systemd tarball), and the examples
directory.
- Mention macOS `.pkg` and Windows ZIP/service packaging in the
packaging line of What works today.
Adds entries for master-only work since 0.2.0 that wasn't captured
yet: Windows and macOS platform support, the outbound LAN gateway
and its packaging, the macOS WireGuard sidecar example, multi-backend
.fips DNS configuration, the node.log_level config, the Nostr UDP
hole punch protocol proposal doc, the MMP report interval retune,
the info-to-debug log demotion, the rekey msg1 gate fix, and the
fipstop ratatui try_init change.
Fixed entries only cover bugs present in 0.2.0. Fixes against
master-only code (BLE reliability work, new sidecar port mapping)
are rolled into their respective Added entries rather than listed
as Fixed, per Keep a Changelog conventions.
Captures three fixes landed on maint since 0.2.0 that were not yet
recorded: the bloom-filter greedy-tree fallback fix, auto-connect
reconnect on graceful disconnect (#60), and fipsctl mesh-address
rejection (#61).
When a user passes an fd00::/8 address as the endpoint for a udp,
tcp, or ethernet transport, the CLI previously echoed success while
the daemon silently failed the bind with EAFNOSUPPORT. Mesh ULAs are
destinations inside the mesh, not reachable transport endpoints.
fipsctl now validates the address up front and prints a clear error
with examples, exiting 1 before the control socket call. Other
transports (tor) are not inspected since they legitimately accept
non-IP endpoints.
Covered by inline tests for bare/bracketed/with-port ULA syntaxes,
non-ULA IPv6, IPv4, hostnames, and the transport filter.
Fixes#61.
handle_disconnect() called remove_active_peer without scheduling a
reconnect, orphaning auto-connect peers on a clean upstream shutdown.
Mirror the pattern from the other three peer-removal paths (link-dead,
decrypt failure, peer restart) which all schedule reconnect after
removal.
Adds test_disconnect_schedules_reconnect regression test that verifies
handle_disconnect populates retry_pending for an auto-connect peer.
Visibility of handle_disconnect bumped to pub(in crate::node) for
direct unit-test access.
Fixes#60.
CI format check was failing because the pinned toolchain
rust-toolchain.toml overrides the `dtolnay/rust-toolchain@stable +
rustfmt` action installation — rustup installs rustfmt onto the
stable channel, but when cargo fmt runs inside the repo, rustup
honors the 1.94.1 pin and does an on-demand install that pulls only
rustc/cargo/rust-std.
Declaring components in rust-toolchain.toml ensures the on-demand
install of the pinned toolchain includes rustfmt.
Replace the resolvectl-only fips-dns.service with a detection script
that configures whichever DNS resolver is available:
1. systemd dns-delegate (systemd >= 258, declarative drop-in)
2. systemd-resolved via resolvectl (most systemd distros)
3. dnsmasq (standalone)
4. NetworkManager with dnsmasq plugin
5. Warning with manual instructions if none found
Service reloads are non-fatal — config is written and the backend
is recorded even if the reload fails, preventing state file cleanup
issues under set -e.
Teardown reads the recorded backend from /run/fips/dns-backend and
reverses the configuration, or cleans up all possible backends if
the state file is missing.
Includes a Docker-based test harness (testing/dns-resolver/test.sh)
covering all five backends across Debian 12, Debian 13, Fedora,
and bare systems.
Fixes#52.
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.
Add a documented macOS sidecar setup under examples so gateway traffic can be routed through a local Docker WireGuard sidecar for development and testing. Generate persistent FIPS and WireGuard key material on first run and keep those local runtime artifacts out of version control.
The entrypoint HTTP server was binding port 80 but gateway-test.sh
curls port 8000. Maint already had 8000; the wrong port was introduced
during a merge to master.
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
Add fips-gateway binary to CI artifact and Docker build. Systemd
service unit with After=fips.service dependency and security
hardening. Debian and AUR package entries.
OpenWrt packaging: procd init script managing dnsmasq forwarding,
proxy NDP, RA route advertisements for the virtual IP pool, and a
global IPv6 prefix on br-lan to work around Android suppressing AAAA
queries on ULA-only networks. Sysctl config for IPv6 forwarding.
Gateway enabled by default in OpenWrt config. Ethernet transport
enabled by default.
Default gateway config section (commented out) in common fips.yaml.
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.
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
When bloom filter candidates existed but none were strictly closer to
the destination in tree-coordinate distance, find_next_hop returned
None without trying greedy tree routing. This caused NoRoute failures
in topologies where the tree parent was closer but not a bloom
candidate. Fall through to tree routing when no bloom candidate makes
progress.
Raise the report interval floor from 100ms to 1000ms and ceiling from
2000ms to 5000ms. The old 100ms floor produced ~600 reports per 60s
parent evaluation cycle — far more than the ~10 needed for EWMA
convergence. The new floor yields ~60 reports/cycle, still well above
the convergence threshold, while reducing BLE overhead by 10×.
Add cold-start transition: first 5 SRTT samples use the 200ms floor
for fast initial convergence, then switch to the 1000ms steady-state
floor. Session-layer intervals unchanged (500ms–10000ms).
Wait for all nodes to reach their expected peer counts instead of
only checking a single node. This prevents false failures on slower
CI runners where remote nodes (especially node E in mesh/chain
topologies) take longer to establish all links.
Wait for all nodes to reach their expected peer counts instead of
only checking a single node. This prevents false failures on slower
CI runners where remote nodes (especially node E in mesh/chain
topologies) take longer to establish all links.
- Add package-linux.yml: builds tarball and .deb for x86_64 and aarch64
on v* tag push, uploads artifacts to GitHub release with checksums
- Make build-tarball.sh target-aware: --target, --version, --arch, --no-build
- Make build-deb.sh target-aware: --target, --version, --no-build
- Configurable strip binary via STRIP env var
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.
The ble feature (bluer crate) pulls in libdbus-sys which cannot
cross-compile with cargo-zigbuild. Disable default features and
explicitly enable only tui for the OpenWrt package build.
- Add package-linux.yml: builds tarball and .deb for x86_64 and aarch64
on v* tag push, uploads artifacts to GitHub release with checksums
- Make build-tarball.sh target-aware: --target, --version, --arch, --no-build
- Make build-deb.sh target-aware: --target, --version, --no-build
- Configurable strip binary via STRIP env var
Expand CONTRIBUTING.md with detailed build prerequisites, Rust toolchain
setup (pinned to 1.94.0), and step-by-step first build instructions.
Add contributing link to README.
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.
- 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.
- 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
Add log_level field to NodeConfig (case-insensitive, default: info).
The daemon now loads config before initializing tracing so the
configured level takes effect. RUST_LOG env var still overrides if
set.
Remove hardcoded Environment=RUST_LOG=info from systemd units and
OpenWrt procd init script — these prevented config-driven log levels
from working.
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.
The ble feature (bluer crate) pulls in libdbus-sys which cannot
cross-compile with cargo-zigbuild. Disable default features and
explicitly enable only tui for the OpenWrt package build.