mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
Merge master into next after v0.3.0 release
Brings v0.3.0 release content forward into the next-branch development line, preserves next's own version state, and updates operator-facing version references to v0.4.0-dev. Kept from master: - docs/releases/release-notes-v0.2.1.md and -v0.3.0.md (cumulative archive of shipped releases) - RELEASE-NOTES.md root mirror at v0.3.0 (tracks the most recent shipped release; will be replaced when the v0.4.0 release cycle begins on this line) - CHANGELOG entries: [0.3.0] and [0.2.1] sections inserted under the existing Breaking and [Unreleased] sections - All code, config, test, and documentation updates from master (openwrt yaml resync, doc-config IP placeholders, etc.) Kept from next (resolved against master's release-prep changes): - Cargo.toml / Cargo.lock at 0.4.0-dev (next's package version) - CHANGELOG ## Breaking section (next-specific v0.4.0 wire-format breaking work) and the empty ## [Unreleased] block for future v0.4.0 non-breaking work Updated to v0.4.0-dev for consistency with Cargo.toml: - README badge (v0.3.0--dev -> v0.4.0--dev) and status-section prose, rewritten to describe v0.4.0 wire-format-breaking work on this branch (Noise XX unification, FMP node profiles, slimmer MMP, extensible bloom-filter encoding) instead of the v0.3.0 testing-and-polishing narrative that applied while v0.3.0 was unreleased - docs/reference/cli-fips.md example version string
This commit is contained in:
+56
-54
@@ -99,6 +99,8 @@ with v0.2.x peers.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.0] - 2026-05-11
|
||||
|
||||
### Added
|
||||
|
||||
#### Mesh Layer (FMP)
|
||||
@@ -296,9 +298,6 @@ with v0.2.x peers.
|
||||
|
||||
#### Packaging and Deployment
|
||||
|
||||
- Linux release artifact workflow: builds x86_64 and aarch64 tarballs
|
||||
and `.deb` packages on `v*` tag push, with SHA-256 checksums
|
||||
- AUR publish workflow for tagged stable releases
|
||||
- Arch Linux AUR packaging for `fips` (release) and `fips-git`
|
||||
(development) packages with sysusers.d/tmpfiles.d integration
|
||||
([#21](https://github.com/jmcorgan/fips/pull/21),
|
||||
@@ -332,9 +331,9 @@ with v0.2.x peers.
|
||||
|
||||
#### Documentation
|
||||
|
||||
- Pre-implementation proposal for NAT traversal using Nostr relays
|
||||
as the signaling channel and STUN for reflexive address discovery
|
||||
(`docs/proposals/`)
|
||||
- `docs/design/port-advertisement-and-nat-traversal.md` documents
|
||||
how nodes find each other through Nostr relays and the
|
||||
STUN-assisted UDP hole punch
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -444,16 +443,6 @@ with v0.2.x peers.
|
||||
(`backoff_base_secs`/`backoff_max_secs` now both `0`); operators
|
||||
with chatty apps generating repeat lookups against unreachable
|
||||
destinations can opt back in
|
||||
- Validate bloom filter fill ratio on FilterAnnounce ingress.
|
||||
Inbound FilterAnnounce messages whose derived false-positive
|
||||
rate exceeds `node.bloom.max_inbound_fpr` (new config field,
|
||||
default 0.05) are rejected silently on the wire, logged at WARN,
|
||||
and counted in a new `bloom.fill_exceeded` counter. A
|
||||
rate-limited WARN also fires if our own outgoing filter's FPR
|
||||
exceeds the cap. `BloomFilter::estimated_count` now takes
|
||||
`max_fpr` and returns `Option<f64>`, returning `None` for
|
||||
saturated filters; this propagates through `compute_mesh_size`
|
||||
into `estimated_mesh_size` (already `Option<u64>`)
|
||||
- The `docs/` tree is reorganised so readers can find content by
|
||||
what they're trying to do: tutorials for new users, how-to guides
|
||||
for specific tasks, reference material for configuration and
|
||||
@@ -627,9 +616,9 @@ with v0.2.x peers.
|
||||
directly is impossible (1:1 NAT)
|
||||
- New `external_addr` field on `transports.udp.*` and
|
||||
`transports.tcp.*` for explicit advertise-as override. Accepts
|
||||
either a bare IP (`"54.183.70.180"` — the configured `bind_addr`
|
||||
either a bare IP (`"198.51.100.1"` — the configured `bind_addr`
|
||||
port is appended) or a full `host:port`
|
||||
(`"54.183.70.180:8443"`). Takes precedence over both the bound
|
||||
(`"198.51.100.1:8443"`). Takes precedence over both the bound
|
||||
address and any STUN-derived autodiscovery. Required for TCP
|
||||
on cloud-NAT setups (AWS EIP, GCP/Azure external IPs) where
|
||||
binding to the public IP directly fails with `EADDRNOTAVAIL`
|
||||
@@ -675,22 +664,6 @@ with v0.2.x peers.
|
||||
matching the Tor `HiddenServicePort` convention) controls the
|
||||
advertised port; operators with non-default virtual ports can
|
||||
override.
|
||||
- Control socket path detection in fipsctl and fipstop now checks for
|
||||
the `/run/fips/` directory instead of the socket file inside it, so
|
||||
users not yet in the `fips` group get a clear "Permission denied"
|
||||
error instead of a misleading "No such file" fallback to
|
||||
`$XDG_RUNTIME_DIR` ([#30](https://github.com/jmcorgan/fips/issues/30),
|
||||
reported by [@Sebastix](https://github.com/Sebastix))
|
||||
- OpenWrt ipk build excluded BLE feature that requires D-Bus, which is
|
||||
unavailable on OpenWrt targets
|
||||
- IPv6 routing policy rule added at TUN setup to protect `fd00::/8`
|
||||
from interception by Tailscale's table 52 default route
|
||||
- Bloom filter routing no longer swallows traffic when no bloom
|
||||
candidate is strictly closer than the current node. `find_next_hop`
|
||||
now falls through to greedy tree routing in that case instead of
|
||||
returning `NoRoute`, which previously caused dropped packets in
|
||||
topologies where the tree parent was closer but not a bloom
|
||||
candidate
|
||||
- TCP-over-FIPS reliability on mesh paths with mixed transport
|
||||
MTUs (e.g. a UDP-1280 hop in the picker set) improved. Three
|
||||
interlocking changes: `Node::transport_mtu()` is now deterministic
|
||||
@@ -728,6 +701,55 @@ with v0.2.x peers.
|
||||
the cooldown window are silent. New `protocol_mismatch_cooldown_secs`
|
||||
config field under `node.discovery.nostr` (default 86400 = 24h),
|
||||
separate from the transient-failure `extended_cooldown_secs`.
|
||||
- `fipstop` now uses `ratatui::try_init()` instead of `ratatui::init()`,
|
||||
so terminal initialization failures (e.g. Docker on macOS Sequoia,
|
||||
or environments without a usable tty) produce a clean error message
|
||||
instead of a hard crash
|
||||
- Spanning-tree updates that change only the internal path between
|
||||
root and leaf — without changing the root or the depth — now
|
||||
propagate to leaves correctly. Previously a leaf could continue
|
||||
routing against a stale internal path until the parent or depth
|
||||
also changed.
|
||||
|
||||
## [0.2.1] - 2026-05-11
|
||||
|
||||
### Added
|
||||
|
||||
- Linux release artifact workflow: builds x86_64 and aarch64 tarballs
|
||||
and `.deb` packages on `v*` tag push, with SHA-256 checksums
|
||||
- AUR publish workflow for tagged stable releases
|
||||
|
||||
### Changed
|
||||
|
||||
- Validate bloom filter fill ratio on FilterAnnounce ingress.
|
||||
Inbound FilterAnnounce messages whose derived false-positive
|
||||
rate exceeds `node.bloom.max_inbound_fpr` (new config field,
|
||||
default 0.05) are rejected silently on the wire, logged at WARN,
|
||||
and counted in a new `bloom.fill_exceeded` counter. A
|
||||
rate-limited WARN also fires if our own outgoing filter's FPR
|
||||
exceeds the cap. `BloomFilter::estimated_count` now takes
|
||||
`max_fpr` and returns `Option<f64>`, returning `None` for
|
||||
saturated filters; this propagates through `compute_mesh_size`
|
||||
into `estimated_mesh_size` (already `Option<u64>`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Control socket path detection in fipsctl and fipstop now checks for
|
||||
the `/run/fips/` directory instead of the socket file inside it, so
|
||||
users not yet in the `fips` group get a clear "Permission denied"
|
||||
error instead of a misleading "No such file" fallback to
|
||||
`$XDG_RUNTIME_DIR` ([#30](https://github.com/jmcorgan/fips/issues/30),
|
||||
reported by [@Sebastix](https://github.com/Sebastix))
|
||||
- OpenWrt ipk build excluded BLE feature that requires D-Bus, which is
|
||||
unavailable on OpenWrt targets
|
||||
- IPv6 routing policy rule added at TUN setup to protect `fd00::/8`
|
||||
from interception by Tailscale's table 52 default route
|
||||
- Bloom filter routing no longer swallows traffic when no bloom
|
||||
candidate is strictly closer than the current node. `find_next_hop`
|
||||
now falls through to greedy tree routing in that case instead of
|
||||
returning `NoRoute`, which previously caused dropped packets in
|
||||
topologies where the tree parent was closer but not a bloom
|
||||
candidate
|
||||
- Auto-connect peers now reconnect after a graceful `Disconnect`
|
||||
notification from the remote side. `handle_disconnect` previously
|
||||
removed the peer without scheduling a reconnect, orphaning the
|
||||
@@ -741,19 +763,10 @@ with v0.2.x peers.
|
||||
bind with `EAFNOSUPPORT`
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61),
|
||||
reported by [@SwapMarket](https://github.com/SwapMarket))
|
||||
- `fipstop` now uses `ratatui::try_init()` instead of `ratatui::init()`,
|
||||
so terminal initialization failures (e.g. Docker on macOS Sequoia,
|
||||
or environments without a usable tty) produce a clean error message
|
||||
instead of a hard crash
|
||||
- Tighten TreeAnnounce ancestry validation to match the spanning
|
||||
tree specification. The receive path now verifies that the
|
||||
ancestry is structurally consistent with the signed parent
|
||||
declaration before mutating tree state.
|
||||
- Spanning-tree updates that change only the internal path between
|
||||
root and leaf — without changing the root or the depth — now
|
||||
propagate to leaves correctly. Previously a leaf could continue
|
||||
routing against a stale internal path until the parent or depth
|
||||
also changed.
|
||||
- Make the tree ancestry acceptance unit test deterministic.
|
||||
`test_tree_announce_validate_semantics_accepts_valid_non_root`
|
||||
generated a random signing identity while pinning the fixed root
|
||||
@@ -762,17 +775,6 @@ with v0.2.x peers.
|
||||
`AncestryRootNotMinimum`. The test now regenerates the identity
|
||||
until its `node_addr` is strictly larger than both the fixed
|
||||
parent and root.
|
||||
- Responder now sends an encrypted `Disconnect` frame on the
|
||||
newly-established Noise session when rejecting a peer in
|
||||
`handle_msg3`, before tearing down. Under Noise XX the responder
|
||||
only learns the initiator's identity from msg3, so by the time an
|
||||
inbound-handshake policy check can reject, the initiator has
|
||||
already received msg2 and promoted its side of the peering. Without
|
||||
an explicit notification the initiator would keep the "connected"
|
||||
state until link-dead timeout fired, producing the
|
||||
`acl-allowlist` CI failure on the `next` branch. The notification
|
||||
allows the initiator's existing `handle_disconnect` path to clean
|
||||
up within one RTT.
|
||||
|
||||
## [0.2.0] - 2026-03-22
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||

|
||||
[](LICENSE)
|
||||
[](https://www.rust-lang.org/)
|
||||
[](#status--roadmap)
|
||||
[](#status--roadmap)
|
||||
|
||||
A self-organizing encrypted mesh network built on Nostr identities,
|
||||
capable of operating over arbitrary transports without central
|
||||
@@ -183,14 +183,16 @@ testing/ Docker-based integration test harnesses + chaos simulation
|
||||
|
||||
## Status & roadmap
|
||||
|
||||
FIPS is at **v0.3.0-dev**. The core protocol works end-to-end over
|
||||
FIPS is at **v0.4.0-dev** on the `next` branch.
|
||||
[v0.3.0](https://github.com/jmcorgan/fips/releases/tag/v0.3.0)
|
||||
has shipped from `master`; this development line carries
|
||||
wire-format-breaking work for v0.4.0 — unified Noise XX handshake
|
||||
at both layers, FMP node profiles, slimmer MMP reports, and an
|
||||
extensible bloom-filter encoding — that will not interoperate with
|
||||
v0.2.x or v0.3.x peers. The core protocol works end-to-end over
|
||||
UDP, TCP, Ethernet, Tor, and Bluetooth on a small live mesh of
|
||||
deployed nodes. v0.3.0 is the testing-and-polishing track for
|
||||
everything accumulated since v0.2.0 on the v0.2.x wire format —
|
||||
Nostr-mediated peer discovery, UDP NAT traversal, peer ACL, the
|
||||
DNS-responder fix, packaging hardening, and discovery rate-limit
|
||||
retuning. New wire-format work is staged on the `next` branch for
|
||||
the post-v0.3.0 release line.
|
||||
deployed nodes. See the CHANGELOG `## Breaking` section for the
|
||||
full list of v0.4.0 wire-format changes in flight.
|
||||
|
||||
### What works today
|
||||
|
||||
|
||||
@@ -0,0 +1,764 @@
|
||||
# FIPS v0.3.0
|
||||
|
||||
**Released**: 2026-05-11
|
||||
|
||||
v0.3.0 is the testing-and-polishing release on the v0.2.x wire format.
|
||||
It widens the platform reach of FIPS from Linux-only to Linux, macOS,
|
||||
Windows, and OpenWrt; adds two large new mesh capabilities (Nostr-mediated
|
||||
peer discovery with UDP NAT traversal, and the `fips-gateway` LAN bridge);
|
||||
ships a default-deny security baseline for the mesh interface; introduces
|
||||
mesh-peer access control; substantially speeds up session-layer crypto and
|
||||
the Linux receive path; and tightens packaging across every supported
|
||||
distribution channel.
|
||||
|
||||
v0.3.0 is wire-compatible with v0.2.x. Mixed meshes interoperate; there
|
||||
is no flag-day upgrade.
|
||||
|
||||
v0.3.0 also rolls forward all changes from the v0.2.1 maintenance
|
||||
release. The sections below cover the cumulative v0.2.0 → v0.3.0
|
||||
delta; the per-section intros call out which entries first shipped
|
||||
in v0.2.1.
|
||||
|
||||
## At a glance
|
||||
|
||||
- 123 commits since v0.2.0 (109 non-merge), spanning 307 files with
|
||||
+44,186 / -4,078 lines.
|
||||
- 10 committers plus 3 issue reporters across feature work, fixes,
|
||||
packaging, and reviews.
|
||||
- 5 new GitHub Actions CI workflows (Linux Package, macOS Package,
|
||||
Windows Package, OpenWrt Package, AUR Publish) plus expanded
|
||||
integration matrices (gateway, NAT-cone, NAT-symmetric, NAT-LAN,
|
||||
rekey-accept-off, `.deb` install across Debian 12/13 + Ubuntu
|
||||
22/24/26, multi-backend `.fips` DNS resolver across the same five
|
||||
distros).
|
||||
- The long-standing systemd-resolved DNS-responder silent-drop is
|
||||
closed end-to-end.
|
||||
- Pre-1.0 control-socket JSON schema change for two query fields;
|
||||
see [Upgrade notes](#upgrade-notes).
|
||||
|
||||
## What's new
|
||||
|
||||
### Mesh discovery and NAT traversal
|
||||
|
||||
Previously, two FIPS nodes could only become peers if they had a way
|
||||
to find each other beforehand: a configured address, a shared LAN
|
||||
segment, or a Bluetooth radio range. v0.3.0 introduces a Nostr-based
|
||||
overlay-discovery channel that lets nodes find each other through any
|
||||
public Nostr relay set, plus a STUN-assisted UDP hole-punching path
|
||||
that connects peers across most consumer NATs.
|
||||
|
||||
Each participating node publishes a signed overlay advert as a Nostr
|
||||
**Kind 37195** parameterized replaceable event. (The kind sits in the
|
||||
application-defined replaceable range and the digits visually spell
|
||||
*FIPS*: 7=F, 1=I, 9=P, 5=S.) The advert lists reachable transport
|
||||
endpoints (UDP, TCP, Tor) and is consumed by other nodes to populate
|
||||
fallback addresses for `via_nostr` peers. Under `policy: open`, the
|
||||
advert cache is also dialed for non-configured peers within a budget
|
||||
cap.
|
||||
|
||||
When both peers are behind NAT, the daemon coordinates a UDP hole
|
||||
punch using NIP-59 gift-wrap signaling for the offer/answer exchange
|
||||
and STUN for reflexive address discovery. A candidate-pair punch
|
||||
planner attempts LAN-private and reflexive paths in parallel; on
|
||||
success the live socket is handed into the standard FIPS UDP transport
|
||||
via a bootstrap-handoff API.
|
||||
|
||||
Operators turn this on with `node.discovery.nostr.enabled: true` and
|
||||
the configured relay set. `policy: open` adds best-effort dialing of
|
||||
non-configured peers seen on the relays. New `peers[].via_nostr` and
|
||||
per-transport `advertise_on_nostr` / `public` flags control what each
|
||||
endpoint contributes to the published advert. Cross-field validation
|
||||
runs at startup to catch mis-configured combinations early.
|
||||
|
||||
A Docker NAT lab covering cone, symmetric, and LAN scenarios is wired
|
||||
into the integration CI matrix. A daemon-side failure-suppression
|
||||
layer (per-npub cooldown after consecutive failures, ±60s clock-skew
|
||||
tolerance, rate-limited WARN logs) keeps relay traffic well-mannered
|
||||
when peers come and go from the open discovery cache. A separate
|
||||
structural cooldown (`protocol_mismatch_cooldown_secs`, default 24h)
|
||||
suppresses retraversal when a punched peer turns out to be running an
|
||||
FMP version this daemon cannot handshake with: the punch completes at
|
||||
the UDP layer, the rx loop spots the version-mismatched packet,
|
||||
reverse-maps to the originating npub, and removes the peer from the
|
||||
next sweep until either side upgrades.
|
||||
|
||||
The auto-connect retry loop pins itself to relay ground truth. Each
|
||||
retry attempt refetches the cached overlay advert against the
|
||||
configured `advert_relays` (one filter query, 2s timeout) before
|
||||
dialing, so a peer whose NAT rebound to a fresh endpoint is recovered
|
||||
on the next retry rather than looping on a stale cached address.
|
||||
`NoTransportForType` triggers a fire-and-forget re-fetch that either
|
||||
replaces or evicts the cache entry. A startup peer-init failure (no
|
||||
operational transport, all addresses unreachable) now schedules a
|
||||
retry instead of leaving the peer in a dead state until the daemon is
|
||||
restarted. Adopted NAT-traversed UDP transports inherit the operator's
|
||||
primary `[transports.udp]` listener config (MTU, recv/send buffer
|
||||
sizes) instead of falling back to the 1280 IPv6-minimum default.
|
||||
|
||||
### Cross-platform reach
|
||||
|
||||
FIPS now ships first-class binaries for **Linux, macOS, Windows, and
|
||||
OpenWrt**.
|
||||
|
||||
- **macOS** support uses the native `utun` TUN interface, raw
|
||||
Ethernet via BPF, a `.pkg` installer with a launchd plist and
|
||||
uninstall script, and an x86_64 cross-compile from arm64 build
|
||||
hosts. A new CI matrix entry runs build and unit-test jobs on
|
||||
macOS hosts.
|
||||
- **Windows** support uses [wintun](https://www.wintun.net/) for the
|
||||
TUN device, a TCP control socket on `localhost:21210` (replacing
|
||||
the Unix domain socket Linux and macOS use), Windows Service
|
||||
lifecycle (`fips.exe --install-service`, `--uninstall-service`,
|
||||
`--service`), and a ZIP package with PowerShell install/uninstall
|
||||
scripts.
|
||||
- **MIPS** atomic-ABI portability lets the daemon build for 32-bit
|
||||
MIPS targets (`mips`, `mipsel`, MIPS32r2) by routing through
|
||||
`portable_atomic`. This unblocks OpenWrt deployments on
|
||||
consumer-grade MIPS routers.
|
||||
- **OpenWrt** packaging gets a procd init with dnsmasq forwarding,
|
||||
proxy NDP, RA route advertisements, and IPv6 forwarding sysctls.
|
||||
The `fips-gateway` is enabled by default in the OpenWrt build.
|
||||
|
||||
### FIPS gateway
|
||||
|
||||
The new `fips-gateway` binary lets unmodified LAN hosts reach FIPS
|
||||
mesh destinations without running the FIPS daemon themselves. Two
|
||||
flows ship together:
|
||||
|
||||
- **Outbound (LAN -> mesh)**: a virtual-IP pool (default
|
||||
`fd01::/112`) is allocated on demand from `.fips`-name DNS lookups.
|
||||
A state-machine lifecycle, conntrack-backed session tracking, proxy
|
||||
NDP on the LAN interface, and TTL-based reclamation handle the
|
||||
bookkeeping. A LAN host that resolves `peer.fips` gets a virtual
|
||||
address it can reach over IP, and the gateway translates the flow
|
||||
to the mesh.
|
||||
- **Inbound (mesh -> LAN)**: new `gateway.port_forwards` config
|
||||
installs prerouting DNAT rules so mesh peers can reach a configured
|
||||
`host:port` on the gateway's LAN. A LAN-side masquerade is added
|
||||
automatically when any forwards are configured, so replies flow
|
||||
back through conntrack.
|
||||
|
||||
A dedicated control socket at `/run/fips/gateway.sock` exposes
|
||||
`show_gateway` and `show_mappings`. `fipstop` adds a Gateway tab with
|
||||
a pool gauge and mappings table.
|
||||
|
||||
The gateway's `dns.listen` source default is now `[::1]:5353`,
|
||||
matching the canonical deployment model: the gateway sits on a host
|
||||
already serving DHCP and DNS to a LAN segment (an OpenWrt AP, a Linux
|
||||
router), port 53 there is taken by the existing resolver, and `.fips`
|
||||
queries are forwarded to the gateway over loopback. The OpenWrt ipk
|
||||
previously overrode the prior `[::]:53` source default in its packaged
|
||||
config; that override is now redundant and has been dropped.
|
||||
Operators on a host without a pre-existing resolver on port 53 can
|
||||
opt back into the wildcard bind by setting `dns.listen: "[::]:53"`
|
||||
explicitly. The new default binds IPv6 loopback only, so forwarders
|
||||
that reach the gateway over IPv4 loopback need an explicit IPv4
|
||||
listen address.
|
||||
|
||||
The cold-boot startup race between `fips.service` and
|
||||
`fips-gateway.service` is handled by a systemd `After=fips.service`
|
||||
ordering, an `ExecStartPre` poll loop that waits up to 30 seconds for
|
||||
the `fips0` interface to appear, and a DNS upstream probe in the
|
||||
gateway itself that retries up to 5 times with 1-second backoff.
|
||||
|
||||
Packaging covers systemd, Debian, AUR, and OpenWrt. The full design
|
||||
is in [`docs/design/fips-gateway.md`](../design/fips-gateway.md).
|
||||
|
||||
### Mesh-interface security baseline
|
||||
|
||||
The FIPS mesh is a flat layer-3 segment. Every authenticated peer can
|
||||
route packets to every other peer's `fips0` address. Peer identity is
|
||||
authenticated end-to-end by the FMP and FSP Noise handshakes, but
|
||||
identity is not authorization. A service on a mesh host that binds to
|
||||
a wildcard address is, by default, reachable from every peer in the
|
||||
mesh.
|
||||
|
||||
v0.3.0 ships an opt-in default-deny baseline that closes this gap on
|
||||
Linux:
|
||||
|
||||
- **`/etc/fips/fips.nft`** is installed as a documented operator
|
||||
conffile. It defines a single `inet fips` nftables table with one
|
||||
chain hooked at `input`, default-denies inbound traffic on
|
||||
`fips0`, and is a no-op for every other interface.
|
||||
- **`fips-firewall.service`** loads it. The unit ships **disabled by
|
||||
default**; activation is an explicit
|
||||
`systemctl enable --now fips-firewall.service`.
|
||||
- Per-service allowances live in **`/etc/fips/fips.d/*.nft`**
|
||||
drop-ins that the baseline includes.
|
||||
|
||||
Choosing opt-in keeps the mesh quick to bring up for evaluation while
|
||||
giving operators a documented, packaged path to lock it down for
|
||||
production. The full design (threat model, rule layout, conntrack
|
||||
handling, drop-in mechanism, and the rationale for a conffile rather
|
||||
than an auto-loaded package side-effect) is in
|
||||
[`docs/design/fips-security.md`](../design/fips-security.md).
|
||||
|
||||
`fipstop`'s Node tab gains a **"Listening on fips0" panel** that
|
||||
surfaces the answer to the operational question "what services on
|
||||
this host are reachable from the mesh, and what does the firewall
|
||||
currently say about each of them?" The panel lists every IPv6
|
||||
listening socket bound to either the wildcard address or this node's
|
||||
`fd00::/8` address, paired with its classification against the
|
||||
running `inet fips` baseline chain: `OPEN` (canonical accept rule),
|
||||
`filt` (falls through to drop), or `filt?` (referenced with matchers
|
||||
the panel cannot fully decompose, e.g. saddr filters or jumps). When
|
||||
`fips-firewall.service` is inactive, a yellow banner above the table
|
||||
reminds the operator that every listener is mesh-exposed; wildcard
|
||||
binds carry a trailing `*` in the Process column. The classifier is
|
||||
built on a new `show_listening_sockets` control query (Linux-only),
|
||||
which is also useful from `fipsctl` for scripting.
|
||||
|
||||
### Peer access control
|
||||
|
||||
Operators can now restrict which mesh peers a node will form direct
|
||||
links with. Optional `/etc/fips/peers.allow` and `/etc/fips/peers.deny`
|
||||
files (TCP-Wrappers style) match against npub, hex pubkey, host
|
||||
alias, or `ALL`. Enforcement runs at three points:
|
||||
|
||||
1. Outbound connect (before dialing).
|
||||
2. Inbound msg1 (the first FMP handshake message from a new peer).
|
||||
3. Outbound msg2 (the response).
|
||||
|
||||
Files reload automatically on mtime change; a new `fipsctl acl show`
|
||||
query reports the effective rule set. A six-node Docker integration
|
||||
harness (`testing/acl/`) exercises allowlist and denylist patterns
|
||||
end-to-end.
|
||||
|
||||
**Important scope distinction**: peer ACLs are an FMP-layer
|
||||
restriction. They control who can establish a *direct link* with this
|
||||
node. They do **not** control session-layer (FSP) reachability through
|
||||
the mesh. A node that denies peer X with an ACL can still receive FSP
|
||||
traffic from X relayed via other peers.
|
||||
|
||||
### Bluetooth Low Energy transport (experimental, Linux)
|
||||
|
||||
A new BLE L2CAP Connection-Oriented Channel transport lets FIPS nodes
|
||||
peer over Bluetooth Low Energy without any IP infrastructure in
|
||||
between. The transport handles per-link MTU negotiation, continuous
|
||||
scan/probe peer discovery with cooldown-based deduplication,
|
||||
continuous advertising, deterministic NodeAddr cross-probe
|
||||
tie-breaker, and a configurable connection pool with eviction.
|
||||
|
||||
This transport is **experimental in v0.3.0**. It is implemented and
|
||||
functional on Linux (BlueZ via `bluer`), but the reliability follow-up
|
||||
logic (probe cooldown, cross-probe tie-breaker, pubkey timeout,
|
||||
continuous advertising semantics, probe-promotion, fail-fast send) is
|
||||
not yet behaviorally tested in CI. Its maturity path is field-driven;
|
||||
please file issues with field reports. macOS BLE support is in
|
||||
development as a separate track and is not part of v0.3.0.
|
||||
|
||||
### UDP transport profiles
|
||||
|
||||
The UDP transport gains posture flags organized around deployment
|
||||
patterns:
|
||||
|
||||
- **Public-facing inbound nodes**: `bind_addr: "0.0.0.0:2121"`,
|
||||
`accept_connections: true` (default), `public: true` for advert
|
||||
publication. v0.3.0 adds STUN-based public-IP autodiscovery so
|
||||
cloud nodes (AWS EIP, GCP, Azure 1:1 NAT) advertise the right
|
||||
address even when the public IP isn't on a host interface.
|
||||
- **Ephemeral leaf nodes**: `outbound_only: true` binds an ephemeral
|
||||
port (`0.0.0.0:0`), refuses inbound msg1, and is never advertised
|
||||
on Nostr regardless of `advertise_on_nostr`. Use this for client
|
||||
postures that should connect outbound only, without exposing an
|
||||
inbound listener on a known port.
|
||||
- **General-purpose nodes**: `accept_connections: false` mirrors the
|
||||
Ethernet/BLE knob without changing the bind address. The Node-level
|
||||
handshake gate carves out msg1 from peers already established on
|
||||
this transport so rekey continues to work.
|
||||
|
||||
Startup validation now rejects `bind_addr` set to a loopback address
|
||||
when at least one peer has a non-loopback UDP address, closing a
|
||||
silent-failure trap from v0.2.0 where Linux's source-address routing
|
||||
check would drop outbound flows from the loopback-bound socket.
|
||||
|
||||
A new `external_addr` field on `transports.udp.*` and
|
||||
`transports.tcp.*` lets operators specify the advertise-as address
|
||||
explicitly. This is useful for UDP as a deterministic alternative to
|
||||
STUN, and required for TCP on cloud-NAT setups (where binding to the
|
||||
public IP fails with `EADDRNOTAVAIL` because the IP isn't on a host
|
||||
interface).
|
||||
|
||||
### `.fips` DNS resolver overhaul
|
||||
|
||||
The IPv6 adapter's `.fips` name resolution has been rebuilt around
|
||||
the constraints of contemporary systemd-based hosts. The default
|
||||
`dns.bind_addr` is now `::1` (IPv6 loopback), and a setup script
|
||||
picks one of five backends in priority order:
|
||||
|
||||
1. systemd-resolved global drop-in
|
||||
(`/etc/systemd/resolved.conf.d/fips.conf`).
|
||||
2. systemd dns-delegate (per-link configuration handed off to
|
||||
systemd-resolved).
|
||||
3. `resolvectl` per-link configuration.
|
||||
4. Standalone `dnsmasq`.
|
||||
5. NetworkManager's dnsmasq plugin.
|
||||
|
||||
Teardown reverses only what setup applied, recorded in a state file
|
||||
at `/run/fips/dns-backend`. A new `testing/dns-resolver/` harness
|
||||
exercises every backend across Debian 12, Debian 13, Ubuntu 22.04,
|
||||
Ubuntu 24.04, and Ubuntu 26.04, so a regression in any of the five
|
||||
backends shows up in CI rather than in the field.
|
||||
|
||||
This overhaul resolves the long-standing silent-drop case where the
|
||||
`resolvectl dns fips0 [<fips0_addr>]:5354` target collided with the
|
||||
daemon's mesh-interface filter on certain systemd-resolved
|
||||
deployments (typically Ubuntu 22 with systemd 249's interface-scoped
|
||||
routing).
|
||||
|
||||
### Operator tooling additions
|
||||
|
||||
A handful of additions land in `fipsctl`, `fipstop`, and the daemon's
|
||||
configuration surface:
|
||||
|
||||
- **`node.log_level`** config field replaces the hardcoded
|
||||
`RUST_LOG=info` previously baked into systemd units and the
|
||||
OpenWrt procd init. The daemon now loads config before
|
||||
initializing tracing so the configured level takes effect.
|
||||
`RUST_LOG` still overrides when set.
|
||||
- **`fipsctl show identity-cache`** is a new query that lists every
|
||||
cached node identity (npub, IPv6 address, display name, LRU age)
|
||||
alongside the configured cache capacity.
|
||||
- **`fipsctl show peers / sessions / cache / routing`** are
|
||||
substantially extended: per-peer security signals (replay
|
||||
suppression count, consecutive decrypt failures), Noise session
|
||||
counters, session indices, rekey lifecycle state, handshake resend
|
||||
counts, K-bit epoch, coords-warmup remaining, drain state, per-peer
|
||||
retry state, per-target lookup detail (attempt, age, last sent),
|
||||
and pending TUN packet queue depth.
|
||||
- **Historical statistics**: in-memory time-series rings on the
|
||||
daemon (1-second × 3600 fast, 1-minute × 1440 slow) cover per-node
|
||||
and per-peer metrics. New `show_stats_*` control-socket queries, a
|
||||
`fipsctl stats list / peers / history` subcommand with Unicode
|
||||
sparkline rendering, and a `fipstop` Graphs tab with btop-style
|
||||
sparklines surface them to the operator.
|
||||
|
||||
### Performance
|
||||
|
||||
Two independent perf threads land in v0.3.0: a session-layer crypto
|
||||
backend swap, and a Linux receive-path overhaul.
|
||||
|
||||
**Session-layer crypto backend.** The ChaCha20-Poly1305 backend used
|
||||
by every FIPS Noise session (end-to-end FSP traffic and link-layer
|
||||
FMP traffic alike) has been swapped from RustCrypto's
|
||||
`chacha20poly1305` crate to `ring 0.17`. ring wraps BoringSSL's
|
||||
hand-tuned ChaCha20-Poly1305 implementation, which dispatches to NEON
|
||||
on aarch64 and AVX2 / AVX-512 on x86_64. Typical throughput is in the
|
||||
3-5 GB/s/core range, versus the ~600-800 MB/s/core RustCrypto soft
|
||||
path on the same hardware.
|
||||
|
||||
Wire format is unchanged. ChaCha20-Poly1305 is byte-deterministic for
|
||||
a given `(key, nonce, plaintext, aad)`, so any correct AEAD
|
||||
implementation produces identical ciphertext. A mixed mesh with some
|
||||
nodes pre-swap and some post-swap interoperates without protocol
|
||||
awareness; v0.3.0 can roll out across a mesh in any order.
|
||||
|
||||
Measurements on an aarch64 Apple Silicon docker target:
|
||||
|
||||
- Two-node TCP single-stream: 437 -> 1097 Mbps (about 2.5×).
|
||||
- Two-node UDP at 1000 Mbit: 599 Mbps with 40% loss -> lossless at
|
||||
line rate.
|
||||
- Three-node ping under bulk-traffic load: 7.68 ms avg / 215 ms max
|
||||
-> 0.72 ms / 3.6 ms max as the relay path stops being crypto-bound.
|
||||
|
||||
No operator-visible action is required; the swap is internal to the
|
||||
session layer.
|
||||
|
||||
**Linux UDP receive path.** The Linux UDP receive path now uses
|
||||
`recvmmsg(2)` with a 32-packet batch in place of single-packet
|
||||
`recvmsg(2)`. A single `readable()` wakeup drains up to 32 datagrams
|
||||
in one syscall before yielding back to the reactor, eliminating the
|
||||
per-packet scheduler-hop and futex cost that previously capped
|
||||
inbound rate at one event per scheduler quantum independent of CPU.
|
||||
`SO_RXQ_OVFL` is sampled once per batch and surfaced through
|
||||
`AsyncUdpSocket::recv_batch` so the existing 1Hz transport-congestion
|
||||
detector continues to feed the per-transport `dropping` flag. macOS
|
||||
and Windows fall through to the per-packet path; `recvmmsg` is
|
||||
Linux-specific.
|
||||
|
||||
**Inner rx-loop drain batching.** `Node::run_rx_loop` drains up to
|
||||
256 additional ready items via `try_recv()` after each
|
||||
`tokio::select!` await fires on the packet and TUN-outbound branches,
|
||||
in a tight inner loop before yielding. Previously the select cost a
|
||||
full scheduler hop and futex per packet, capping throughput at one
|
||||
event per scheduler quantum with the worker near-idle. `biased`
|
||||
ordering keeps data-plane branches priority over tick / control / DNS
|
||||
under sustained load; the 256 cap keeps the worker on a busy stream
|
||||
between yield points (about 400 KB of contiguous traffic) while still
|
||||
bounding the inner loop so a flood on one branch cannot starve the
|
||||
periodic tick or control socket.
|
||||
|
||||
**Eager `pubkey_full` precompute.** `PeerIdentity::pubkey_full()`
|
||||
precomputes the parity-aware full secp256k1 public key at
|
||||
construction in `from_pubkey`. Previously the method fell through to
|
||||
an EC point parse on every call when the full key wasn't passed at
|
||||
construction (i.e. for every peer constructed from an npub or x-only
|
||||
key), about 6% of per-packet CPU on the bulk-data send path for a
|
||||
value that never changed after construction. The same parse already
|
||||
runs at construction inside `NodeAddr::from_pubkey`, so the cost is
|
||||
paid once where it would be paid anyway.
|
||||
|
||||
These three changes are a coordinated set: the syscall batching
|
||||
removes the per-packet kernel cost, the inner-loop drain removes the
|
||||
per-packet scheduler cost, and the pubkey-cache change removes the
|
||||
per-packet crypto-derivation cost. Like the AEAD swap, they are all
|
||||
internal and require no operator action.
|
||||
|
||||
### Examples
|
||||
|
||||
- **macOS WireGuard companion** ([#51](https://github.com/jmcorgan/fips/pull/51)):
|
||||
run FIPS in a local Docker container and route `.fips` traffic
|
||||
from the macOS host through a WireGuard tunnel to the container's
|
||||
`fips0`. Only traffic destined for `fd00::/8` transits the
|
||||
companion; regular internet traffic continues to use the host
|
||||
network. Persistent FIPS and WireGuard key material is generated
|
||||
on first run.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **`docs/design/port-advertisement-and-nat-traversal.md`**
|
||||
documents how nodes find each other through Nostr relays and the
|
||||
STUN-assisted UDP hole punch.
|
||||
- **`docs/design/fips-gateway.md`** documents the gateway's virtual
|
||||
IP pool, lifecycle, control surface, and packaging.
|
||||
- **`docs/design/fips-security.md`** documents the mesh-interface
|
||||
security posture, threat model, default-deny baseline, and drop-in
|
||||
workflow.
|
||||
- **`CONTRIBUTING.md`** has been expanded with build prerequisites,
|
||||
Rust toolchain setup, and first-build steps.
|
||||
|
||||
The `docs/` tree has been reorganized end-to-end into four sections
|
||||
(*tutorials / how-to / reference / design*) with a new
|
||||
[`docs/getting-started.md`](../getting-started.md) and per-section
|
||||
landing pages. Content was reconciled against current source:
|
||||
protocol-layer details, wire-format diagrams, configuration knobs,
|
||||
and CLI references were brought back into agreement with the
|
||||
implementation. See [Documentation pointers](#documentation-pointers)
|
||||
below for entry points by reader intent.
|
||||
|
||||
## Behavior changes worth flagging
|
||||
|
||||
These default-config changes affect every operator on upgrade, even
|
||||
those with no explicit configuration. Two items below — bloom-filter
|
||||
fill-ratio validation and TreeAnnounce ancestry validation — first
|
||||
shipped in v0.2.1 and roll forward into v0.3.0; the rest are
|
||||
v0.3.0-net-new.
|
||||
|
||||
- **Discovery rate-limiting** has been retuned to be less aggressive
|
||||
at cold start. v0.2.0 used a single-lookup-with-internal-retry
|
||||
model where a timed-out lookup during bloom-filter propagation
|
||||
could suppress retries for 30 seconds while none of the reset
|
||||
triggers fired on a stable post-handshake topology. v0.3.0
|
||||
replaces this with a per-attempt timeout sequence
|
||||
(`node.discovery.attempt_timeouts_secs`, default `[1, 2, 4, 8]`,
|
||||
15s total). Each attempt sends a fresh `LookupRequest` with a new
|
||||
`request_id`, letting successive attempts take different
|
||||
forwarding paths as the bloom and tree state evolve. Post-failure
|
||||
suppression is **off by default**; operators with chatty
|
||||
applications can opt back in via `backoff_base_secs` /
|
||||
`backoff_max_secs`.
|
||||
- **MMP report intervals** are retuned for constrained transports.
|
||||
The steady-state floor moves from 100ms to 1000ms, the ceiling
|
||||
from 2000ms to 5000ms, with a cold-start phase running 200ms for
|
||||
the first 5 SRTT samples. This reduces BLE overhead by roughly
|
||||
10× while keeping reports well above the EWMA convergence
|
||||
threshold. Session-layer MMP intervals are unchanged.
|
||||
- **Bloom filter fill-ratio validation** runs on every inbound
|
||||
`FilterAnnounce`. Filters whose derived false-positive rate
|
||||
exceeds `node.bloom.max_inbound_fpr` (default 0.05) are rejected
|
||||
silently on the wire, logged at WARN, and counted in a new
|
||||
`bloom.fill_exceeded` counter. A rate-limited WARN also fires
|
||||
when the local outgoing filter exceeds the cap.
|
||||
- **TreeAnnounce ancestry validation** is now run before tree-state
|
||||
mutation, enforcing ancestry-self-match, root-single-entry,
|
||||
parent-second-entry, and root-is-minimum-NodeAddr. Non-conforming
|
||||
announces are rejected with a WARN. Mixed v0.2.0 / v0.2.1 / v0.3.0
|
||||
meshes may produce WARN log lines on the v0.2.1+ side until all
|
||||
peers upgrade; behavior is correct, log noise only.
|
||||
- **Log noise reduction**: 35 info-level log messages have been
|
||||
demoted to debug (handshake cross-connection mechanics, periodic
|
||||
MMP telemetry, TUN/transport shutdown, retry scheduling). The
|
||||
default `RUST_LOG` in systemd units is now `info`, where it
|
||||
previously ran at `debug`. Operator-visible info output now
|
||||
focuses on lifecycle events, peer promotions, session
|
||||
establishment, parent switches, and transport start/stop.
|
||||
|
||||
## Notable bug fixes
|
||||
|
||||
These pre-existing v0.2.0 bugs are worth singling out because they
|
||||
either affected real-world deployments or produced misleading
|
||||
operator experiences. The CHANGELOG has the exhaustive list; this is
|
||||
the operator-relevant subset. Four items below first shipped in
|
||||
v0.2.1 and roll forward into v0.3.0: auto-connect Disconnect-reconnect,
|
||||
`fipsctl connect` mesh-address rejection, `fd00::/8` routing
|
||||
protection from Tailscale interception, and bloom-filter routing
|
||||
greedy-tree fallback. The control-socket path-detection fix landed
|
||||
in v0.2.1 as well, and the unified resolver below is the v0.3.0
|
||||
refactor that builds on it.
|
||||
|
||||
- **DNS responder silent-drop on systemd-resolved** is fixed: the
|
||||
responder no longer drops queries on Ubuntu 22 / Debian 13 and
|
||||
similar deployments where systemd applies interface-scoped
|
||||
routing. Default bind moves to `::1`; new global drop-in backend
|
||||
available ([#52](https://github.com/jmcorgan/fips/issues/52),
|
||||
[#77](https://github.com/jmcorgan/fips/issues/77)).
|
||||
- **Auto-connect peers reconnect after a graceful Disconnect.**
|
||||
Previously, a clean upstream shutdown left the auto-connect peer
|
||||
orphaned; only the link-dead, decrypt-fail, and peer-restart
|
||||
paths scheduled a reconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **`fipsctl connect` rejects FIPS mesh addresses** (`fd00::/8`)
|
||||
for `udp`, `tcp`, and `ethernet` transports with a clear error
|
||||
message, instead of echoing success while the daemon silently
|
||||
failed the bind with `EAFNOSUPPORT`
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **Default control-socket path resolution unified.** Daemon and
|
||||
client tools now share a single resolver, eliminating a divergence
|
||||
where `fipsctl` / `fipstop` could connect to a socket the daemon
|
||||
never bound (notably on dev runs with `XDG_RUNTIME_DIR` set, or
|
||||
after a prior packaged install left a root-owned `/run/fips`
|
||||
behind). Canonical order is
|
||||
`/run/fips` -> `$XDG_RUNTIME_DIR/fips/` -> `/tmp/fips-<name>`. The
|
||||
`/run/fips` arm is selected by directory existence; the kernel
|
||||
enforces actual access at `connect(2)` time, so users not yet in
|
||||
the `fips` group get a clear `EACCES` rather than a silent path
|
||||
mismatch and a misleading `No such file` fallback to
|
||||
`$XDG_RUNTIME_DIR`. `XDG_RUNTIME_DIR` is validated as an existing
|
||||
directory before being used so stale post-logout values are
|
||||
treated as missing. The deployed fleet is unaffected: packaged
|
||||
configs set `node.control.socket_path` explicitly
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30), reported by
|
||||
[@Sebastix](https://github.com/Sebastix)).
|
||||
- **`fd00::/8` routing protected from Tailscale interception.** The
|
||||
daemon installs an IPv6 routing-policy rule
|
||||
(`ip -6 rule to fd00::/8 lookup main priority 5265`) at TUN
|
||||
setup, so Tailscale's table 52 default route can no longer divert
|
||||
mesh traffic.
|
||||
- **TCP-over-FIPS reliability on mixed-MTU paths** is markedly
|
||||
improved. Four interlocking changes ship together:
|
||||
`Node::transport_mtu()` is now deterministic across daemon
|
||||
restarts (min across operational transports rather than
|
||||
insertion-order-dependent); the TCP MSS clamp at the TUN boundary
|
||||
reads per-destination path MTU instead of a single global ceiling;
|
||||
reactive `MtuExceeded` from forwarders is mirrored back into the
|
||||
TUN-side `path_mtu_lookup` so later flows pick up forward-path
|
||||
bottlenecks without re-discovery; and the proactive end-to-end
|
||||
`PathMtuNotification` echoed by the destination is mirrored into
|
||||
the same TUN-side store. Without that fourth piece, on long-lived
|
||||
stable paths where the destination's echo had tightened the
|
||||
session MTU but no transit router had emitted a fresh
|
||||
`MtuExceeded`, new TCP flows opened in that window were clamped by
|
||||
the staler discovery-time value. The proactive mirror uses the
|
||||
same tighter-only semantics as the reactive mirror, so it never
|
||||
loosens the clamp. The Windows TUN reader receives the same
|
||||
per-destination plumbing.
|
||||
- **Bloom filter routing greedy-tree fallback.** `find_next_hop` no
|
||||
longer returns `NoRoute` when the bloom candidate set is non-empty
|
||||
but no candidate is strictly closer than the current node; it
|
||||
falls through to greedy tree routing instead. Previously, this
|
||||
caused dropped packets in topologies where the tree parent was
|
||||
closer but not a bloom candidate.
|
||||
- **`fipstop` graceful tty-init failure.** `ratatui::try_init()`
|
||||
produces a clean error message instead of a hard crash when
|
||||
terminal initialization fails (Docker on macOS Sequoia, ttyless
|
||||
environments).
|
||||
- **TreeAnnounce ancestry on self-root transitions.** When a node
|
||||
had no smaller-NodeAddr peer to use as a parent, the spanning-tree
|
||||
state correctly promoted it to root, but the ancestry advertised
|
||||
on the next `TreeAnnounce` still referenced its previous parent's
|
||||
path. Receiving peers rejected the announce as
|
||||
`invalid ancestry: advertised root X is not the minimum path entry
|
||||
Y`, blocking mesh transit on any path that needed to traverse the
|
||||
node. The self-root transition is now detected explicitly in
|
||||
`TreeState::become_root` and the advertised ancestry rebuilt to
|
||||
start from self; the MMP receive handler corrects stale ancestry
|
||||
inherited across reconnect eagerly rather than waiting for the
|
||||
next observation tick.
|
||||
- **Spanning-tree internal-path updates** that change only the
|
||||
internal path between root and leaf (without changing the root or
|
||||
the depth) now propagate to leaves correctly. Previously, a leaf
|
||||
could continue routing against a stale internal path until the
|
||||
parent or depth also changed.
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
Operator-actionable items when moving from v0.2.x to v0.3.0:
|
||||
|
||||
- **Control socket JSON schema (breaking, pre-1.0).**
|
||||
- `show_cache` response field `entries` has changed type from a
|
||||
`u64` count to an array of entry objects. The previous scalar
|
||||
value is now in a new `count` field.
|
||||
- `show_routing` response field `pending_lookups` has changed
|
||||
type from a `u64` count to an array of per-target lookup
|
||||
objects.
|
||||
- External tooling parsing these fields as numbers must be
|
||||
updated. In-tree `fipstop` is adjusted to the new schema. The
|
||||
control-socket interface remains pre-1.0 and is not covered by
|
||||
stability guarantees.
|
||||
|
||||
- **Cargo feature flags removed.** `tui`, `ble`, `gateway`, and
|
||||
`nostr-discovery` are gone. Subsystem inclusion is now driven by
|
||||
platform `cfg` gates, so plain `cargo build` compiles everything
|
||||
available on the target without `--features` invocations.
|
||||
Source-build tooling that passed any of these features should be
|
||||
updated to omit them.
|
||||
|
||||
- **Discovery rate-limiting defaults changed.** Post-failure
|
||||
suppression is **off by default**
|
||||
(`node.discovery.backoff_base_secs: 0`, `backoff_max_secs: 0`).
|
||||
Operators relying on the prior 30s base / 300s cap behavior must
|
||||
set those fields explicitly. The per-attempt sequence
|
||||
(`attempt_timeouts_secs`, default `[1, 2, 4, 8]`) now governs
|
||||
cold-start lookup behavior.
|
||||
|
||||
- **`.fips` DNS bind address default changed.** The default
|
||||
`dns.bind_addr` is now `::1`. Operators with explicit overrides
|
||||
of this field should review them; many existing overrides were
|
||||
workarounds for the silent-drop bug that this release fixes
|
||||
properly.
|
||||
|
||||
- **Gateway `dns.listen` source default changed.** The
|
||||
`fips-gateway` `dns.listen` default is now `[::1]:5353` (was
|
||||
`[::]:53`), matching the canonical deployment model where a
|
||||
pre-existing resolver on the host already owns port 53. The
|
||||
OpenWrt ipk previously overrode this in its packaged config; the
|
||||
override is now redundant and has been dropped. Operators on a
|
||||
host without a pre-existing resolver on port 53 can opt back into
|
||||
the wildcard bind by setting `dns.listen: "[::]:53"` explicitly.
|
||||
The new default binds IPv6 loopback only, so forwarders that
|
||||
reach the gateway over IPv4 loopback need an explicit IPv4 listen
|
||||
address.
|
||||
|
||||
- **systemd unit log level.** The shipped systemd units no longer
|
||||
hardcode `RUST_LOG=info`; the daemon's effective log level is
|
||||
driven by `node.log_level` (default `info`). `RUST_LOG`, when
|
||||
set, still overrides.
|
||||
|
||||
- **UDP transport `bind_addr` validation.** Startup now rejects a
|
||||
`bind_addr` set to a loopback address when at least one peer has
|
||||
a non-loopback UDP address. Operators who configured a loopback
|
||||
UDP bind as a workaround should switch to `outbound_only: true`
|
||||
for the same effect, plus the correct semantics (kernel-assigned
|
||||
ephemeral port, refuses inbound, never advertised).
|
||||
|
||||
- **Tor advert port.** If the Tor `HiddenServicePort` virtual port
|
||||
isn't 443, set `transports.tor.advertised_port` to match. The
|
||||
default is 443 and matches the conventional virtual-port choice.
|
||||
|
||||
## Documentation pointers
|
||||
|
||||
v0.3.0 ships a `docs/` tree reorganized into four sections
|
||||
(*tutorials / how-to / reference / design*). A new top-level
|
||||
[`docs/getting-started.md`](../getting-started.md) and per-section
|
||||
landing pages anchor the entry points.
|
||||
|
||||
Entry points by reader intent:
|
||||
|
||||
- **New users**: [`docs/getting-started.md`](../getting-started.md)
|
||||
and [`docs/tutorials/`](../tutorials/) cover guided introductions
|
||||
for bringing up your first node, joining the test mesh,
|
||||
advertising a node over Nostr, hosting a service, deploying a
|
||||
gateway, walking through the IPv6 adapter, and resolving peers
|
||||
via Nostr.
|
||||
- **Operators with a specific task**:
|
||||
[`docs/how-to/`](../how-to/) holds task-driven guides for enabling
|
||||
Nostr discovery, deploying the gateway, troubleshooting the
|
||||
gateway, deploying a Tor onion, hosting aliases, persistent
|
||||
identity, running unprivileged, setting up a Bluetooth peer,
|
||||
enabling the mesh firewall, tuning UDP buffers, and diagnosing
|
||||
MTU issues.
|
||||
- **Reference lookups**: [`docs/reference/`](../reference/) holds
|
||||
the config field reference, control-socket query reference, the
|
||||
`fips`, `fipsctl`, `fipstop`, and `fips-gateway` CLI references,
|
||||
and the protocol diagram set.
|
||||
- **Architectural background**: [`docs/design/`](../design/) holds
|
||||
design rationale for FIPS as a whole, FMP and FSP, the spanning
|
||||
tree, bloom-filter discovery, transports, the IPv6 adapter, the
|
||||
Nostr discovery layer, and the gateway.
|
||||
- **Security**: [`docs/design/fips-security.md`](../design/fips-security.md)
|
||||
documents the mesh-interface security baseline, threat model, and
|
||||
drop-in workflow.
|
||||
|
||||
## Getting v0.3.0
|
||||
|
||||
- **Linux x86_64 / aarch64**: `.deb` and tarball at the
|
||||
[v0.3.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.3.0).
|
||||
- **Arch Linux**: `fips` from the AUR.
|
||||
- **macOS**: `.pkg` at the v0.3.0 release page.
|
||||
- **Windows**: ZIP at the v0.3.0 release page.
|
||||
- **OpenWrt**: `.ipk` at the v0.3.0 release page.
|
||||
- **From source**: `cargo build --release` from a checkout of the
|
||||
v0.3.0 tag.
|
||||
|
||||
The full per-commit changelog lives in
|
||||
[`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at
|
||||
[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips).
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed code, packaging work, bug reports,
|
||||
or reviews to this release.
|
||||
|
||||
**Code and packaging**:
|
||||
|
||||
- [@jcorgan](https://github.com/jmcorgan): release shepherd, Nostr
|
||||
discovery / NAT traversal, `fips-gateway`, ACL infrastructure,
|
||||
packaging, security baseline, BLE follow-ups.
|
||||
- [@Origami74](https://github.com/Origami74): macOS platform support,
|
||||
from-source Docker companion build and `fipstop` terminal-init
|
||||
handling, gateway co-development, OpenWrt BLE-feature build fix,
|
||||
AUR-workflow follow-ups.
|
||||
- [@jodobear](https://github.com/jodobear): Linux release-artifact
|
||||
workflow and target-aware build scripts, CONTRIBUTING.md
|
||||
expansion, rekey integration-test stabilization.
|
||||
- [@tidley](https://github.com/tidley): Nostr-mediated overlay
|
||||
discovery and UDP NAT traversal
|
||||
([#53](https://github.com/jmcorgan/fips/pull/53)).
|
||||
- [@alexxie16](https://github.com/alexxie16): peer ACL enforcement
|
||||
([#50](https://github.com/jmcorgan/fips/pull/50)),
|
||||
macOS WireGuard companion example
|
||||
([#51](https://github.com/jmcorgan/fips/pull/51)),
|
||||
follow-up ([#67](https://github.com/jmcorgan/fips/pull/67)).
|
||||
- [@osh](https://github.com/osh): diagnostic queries for security
|
||||
validation and mesh debugging
|
||||
([#42](https://github.com/jmcorgan/fips/pull/42)).
|
||||
- [@OceanSlim](https://github.com/0ceanSlim): Windows platform
|
||||
support ([#45](https://github.com/jmcorgan/fips/pull/45)).
|
||||
- [@mmalmi](https://github.com/mmalmi): ring AEAD backend
|
||||
([#80](https://github.com/jmcorgan/fips/pull/80)),
|
||||
hot-path drain batching + recvmmsg + eager pubkey_full
|
||||
([#81](https://github.com/jmcorgan/fips/pull/81)),
|
||||
TreeAnnounce self-root ancestry + overlay-advert retry hygiene
|
||||
([#82](https://github.com/jmcorgan/fips/pull/82)),
|
||||
NAT-traversal MTU inheritance
|
||||
([#83](https://github.com/jmcorgan/fips/pull/83)).
|
||||
- [@dskvr](https://github.com/dskvr): initial Arch Linux AUR
|
||||
packaging ([#21](https://github.com/jmcorgan/fips/pull/21)) and
|
||||
the AUR publish workflow.
|
||||
- [@SatsAndSports](https://github.com/SatsAndSports): rekey
|
||||
message-1 admit fix on non-accepting transports
|
||||
([#49](https://github.com/jmcorgan/fips/pull/49)),
|
||||
TreeAnnounce semantic validation, gateway test image fix
|
||||
([#69](https://github.com/jmcorgan/fips/pull/69)).
|
||||
- [@andrewheadricke](https://github.com/andrewheadricke): MIPS
|
||||
atomic-ABI portability via `portable_atomic`
|
||||
([#62](https://github.com/jmcorgan/fips/pull/62)).
|
||||
- [@sh1ftred](https://github.com/sh1ftred): Arch packaging namcap
|
||||
fixes ([#63](https://github.com/jmcorgan/fips/pull/63)).
|
||||
- [@oleksky](https://github.com/oleksky): macOS WireGuard companion
|
||||
collaboration on [#51](https://github.com/jmcorgan/fips/pull/51).
|
||||
|
||||
**Issue reports that drove fixes in this release**:
|
||||
|
||||
- [@deavmi](https://github.com/deavmi): MIPS daemon build support
|
||||
([#26](https://github.com/jmcorgan/fips/issues/26)).
|
||||
- [@Sebastix](https://github.com/Sebastix): fipsctl/fipstop
|
||||
control-socket path detection
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30)).
|
||||
- [@SwapMarket](https://github.com/SwapMarket): auto-connect
|
||||
reconnect after graceful disconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60)) and
|
||||
fipsctl mesh-address rejection
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61)).
|
||||
@@ -28,7 +28,7 @@ controlled through the standard service control manager.
|
||||
| Flag | Argument | Description |
|
||||
| ---- | -------- | ----------- |
|
||||
| `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. |
|
||||
| `-V` | — | Print the short version (e.g. `0.3.0-dev (rev abcdef1)`). |
|
||||
| `-V` | — | Print the short version (e.g. `0.4.0-dev (rev abcdef1)`). |
|
||||
| `--version` | — | Print the long version: short version plus build target triple. |
|
||||
| `-h`, `--help` | — | Print usage and exit. |
|
||||
| `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. |
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# FIPS v0.2.1
|
||||
|
||||
**Released**: 2026-05-11
|
||||
|
||||
v0.2.1 is a maintenance release on the v0.2.x line. No new features
|
||||
and no wire-format changes; operators running v0.2.0 can upgrade in
|
||||
place. The release rolls up bug fixes and operational hardening for
|
||||
issues surfaced in v0.2.0 deployments, plus a bloom-filter fill-ratio
|
||||
validation that protects mesh-size estimates from saturated-filter
|
||||
inputs.
|
||||
|
||||
## At a glance
|
||||
|
||||
- 22 commits since v0.2.0, 5 committers plus 2 issue reporters.
|
||||
- All changes are backwards-compatible with v0.2.0 on the wire.
|
||||
- Bloom filter fill-ratio validation hardens the FilterAnnounce
|
||||
ingress path.
|
||||
- TreeAnnounce ancestry validation tightened to match the
|
||||
spanning-tree specification.
|
||||
- Signed-tarball + `.deb` artifact workflow added for tagged
|
||||
releases; AUR auto-publish on stable tags.
|
||||
|
||||
## Behavior changes worth flagging
|
||||
|
||||
- **Bloom filter fill-ratio validation** runs on every inbound
|
||||
`FilterAnnounce`. Filters whose derived false-positive rate exceeds
|
||||
`node.bloom.max_inbound_fpr` (new config field, default `0.05`) are
|
||||
rejected silently on the wire, logged at WARN, and counted in a
|
||||
new `bloom.fill_exceeded` counter. A rate-limited WARN also fires
|
||||
when the local outgoing filter exceeds the cap.
|
||||
`BloomFilter::estimated_count` now takes `max_fpr` and returns
|
||||
`Option<f64>`, returning `None` for saturated filters; this
|
||||
propagates through `compute_mesh_size` into `estimated_mesh_size`.
|
||||
- **TreeAnnounce ancestry validation** is now run before tree-state
|
||||
mutation, enforcing ancestry-self-match, root-single-entry,
|
||||
parent-second-entry, and root-is-minimum-NodeAddr. Non-conforming
|
||||
announces are rejected with a WARN. Mixed v0.2.0 / v0.2.1 meshes
|
||||
may produce WARN log lines on the v0.2.1 side until all peers
|
||||
upgrade; behavior is correct, log noise only.
|
||||
|
||||
## Notable bug fixes
|
||||
|
||||
- **Control socket path detection** in `fipsctl` and `fipstop` now
|
||||
checks for the `/run/fips/` directory instead of the socket file
|
||||
inside it. Users not yet in the `fips` group get a clear
|
||||
"Permission denied" error instead of a misleading "No such file"
|
||||
fallback to `$XDG_RUNTIME_DIR`
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30), reported by
|
||||
[@Sebastix](https://github.com/Sebastix)).
|
||||
- **`fd00::/8` routing protected from Tailscale interception.** The
|
||||
daemon installs an IPv6 routing-policy rule
|
||||
(`ip -6 rule to fd00::/8 lookup main priority 5265`) at TUN setup,
|
||||
so Tailscale's table 52 default route can no longer divert mesh
|
||||
traffic.
|
||||
- **Bloom filter routing greedy-tree fallback.** `find_next_hop` no
|
||||
longer returns `NoRoute` when the bloom candidate set is non-empty
|
||||
but no candidate is strictly closer than the current node; it
|
||||
falls through to greedy tree routing instead. Previously, this
|
||||
caused dropped packets in topologies where the tree parent was
|
||||
closer but not a bloom candidate.
|
||||
- **Auto-connect peers reconnect after a graceful Disconnect.**
|
||||
Previously, a clean upstream shutdown left the auto-connect peer
|
||||
orphaned; only the link-dead, decrypt-fail, and peer-restart paths
|
||||
scheduled a reconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **`fipsctl connect` rejects FIPS mesh addresses** (`fd00::/8`) for
|
||||
`udp`, `tcp`, and `ethernet` transports with a clear error message
|
||||
instead of echoing success while the daemon silently failed the
|
||||
bind with `EAFNOSUPPORT`
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **OpenWrt ipk** cross-compiles cleanly again after excluding the
|
||||
BLE feature that requires D-Bus, which is unavailable on OpenWrt
|
||||
targets.
|
||||
|
||||
## Packaging
|
||||
|
||||
- **Linux release artifact workflow** builds x86_64 and aarch64
|
||||
tarballs and `.deb` packages on `v*` tag push, with SHA-256
|
||||
checksums, and publishes them to the GitHub release page.
|
||||
- **AUR publish workflow** auto-publishes the `fips` PKGBUILD on
|
||||
stable `v*` tags.
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
Operator-actionable items when moving from v0.2.0 to v0.2.1:
|
||||
|
||||
- **Bloom filter fill-ratio cap (default 0.05).** Inbound
|
||||
`FilterAnnounce` messages whose derived FPR exceeds the cap are
|
||||
rejected silently on the wire. Operators with unusually saturated
|
||||
filters in the field may want to confirm that the default applies
|
||||
cleanly to their deployment; check the new `bloom.fill_exceeded`
|
||||
counter if mesh-size estimates drift after upgrade.
|
||||
- **TreeAnnounce ancestry tightening.** Mixed v0.2.0 / v0.2.1 meshes
|
||||
may produce WARN log lines on the v0.2.1 side until all peers
|
||||
upgrade. Behavior is correct, log noise only.
|
||||
|
||||
## Getting v0.2.1
|
||||
|
||||
- **Linux x86_64 / aarch64**: `.deb` and tarball at the
|
||||
[v0.2.1 release page](https://github.com/jmcorgan/fips/releases/tag/v0.2.1).
|
||||
- **Arch Linux**: `fips` from the AUR.
|
||||
- **OpenWrt**: `.ipk` at the v0.2.1 release page.
|
||||
- **From source**: `cargo build --release` from a checkout of the
|
||||
v0.2.1 tag.
|
||||
|
||||
The full per-commit changelog lives in
|
||||
[`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at
|
||||
[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips).
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed code or bug reports to this
|
||||
release.
|
||||
|
||||
**Code and packaging**:
|
||||
|
||||
- [@jcorgan](https://github.com/jmcorgan): release shepherd, bloom
|
||||
fill-ratio validation, auto-connect reconnect fix, `fipsctl`
|
||||
mesh-address rejection, control-socket path detection,
|
||||
Tailscale-vs-`fd00::/8` routing policy, bloom routing greedy
|
||||
fallback, rustfmt baseline.
|
||||
- [@Origami74](https://github.com/Origami74): OpenWrt ipk
|
||||
BLE-feature build fix.
|
||||
- [@jodobear](https://github.com/jodobear): Linux release-artifact
|
||||
workflow and target-aware build scripts.
|
||||
- [@dskvr](https://github.com/dskvr): AUR publish workflow.
|
||||
- [@SatsAndSports](https://github.com/SatsAndSports): TreeAnnounce
|
||||
semantic validation.
|
||||
|
||||
**Issue reports that drove fixes in this release**:
|
||||
|
||||
- [@Sebastix](https://github.com/Sebastix): `fipsctl` / `fipstop`
|
||||
control-socket path detection
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30)).
|
||||
- [@SwapMarket](https://github.com/SwapMarket): auto-connect
|
||||
reconnect after graceful disconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60)) and
|
||||
`fipsctl` mesh-address rejection
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61)).
|
||||
@@ -0,0 +1,764 @@
|
||||
# FIPS v0.3.0
|
||||
|
||||
**Released**: 2026-05-11
|
||||
|
||||
v0.3.0 is the testing-and-polishing release on the v0.2.x wire format.
|
||||
It widens the platform reach of FIPS from Linux-only to Linux, macOS,
|
||||
Windows, and OpenWrt; adds two large new mesh capabilities (Nostr-mediated
|
||||
peer discovery with UDP NAT traversal, and the `fips-gateway` LAN bridge);
|
||||
ships a default-deny security baseline for the mesh interface; introduces
|
||||
mesh-peer access control; substantially speeds up session-layer crypto and
|
||||
the Linux receive path; and tightens packaging across every supported
|
||||
distribution channel.
|
||||
|
||||
v0.3.0 is wire-compatible with v0.2.x. Mixed meshes interoperate; there
|
||||
is no flag-day upgrade.
|
||||
|
||||
v0.3.0 also rolls forward all changes from the v0.2.1 maintenance
|
||||
release. The sections below cover the cumulative v0.2.0 → v0.3.0
|
||||
delta; the per-section intros call out which entries first shipped
|
||||
in v0.2.1.
|
||||
|
||||
## At a glance
|
||||
|
||||
- 123 commits since v0.2.0 (109 non-merge), spanning 307 files with
|
||||
+44,186 / -4,078 lines.
|
||||
- 10 committers plus 3 issue reporters across feature work, fixes,
|
||||
packaging, and reviews.
|
||||
- 5 new GitHub Actions CI workflows (Linux Package, macOS Package,
|
||||
Windows Package, OpenWrt Package, AUR Publish) plus expanded
|
||||
integration matrices (gateway, NAT-cone, NAT-symmetric, NAT-LAN,
|
||||
rekey-accept-off, `.deb` install across Debian 12/13 + Ubuntu
|
||||
22/24/26, multi-backend `.fips` DNS resolver across the same five
|
||||
distros).
|
||||
- The long-standing systemd-resolved DNS-responder silent-drop is
|
||||
closed end-to-end.
|
||||
- Pre-1.0 control-socket JSON schema change for two query fields;
|
||||
see [Upgrade notes](#upgrade-notes).
|
||||
|
||||
## What's new
|
||||
|
||||
### Mesh discovery and NAT traversal
|
||||
|
||||
Previously, two FIPS nodes could only become peers if they had a way
|
||||
to find each other beforehand: a configured address, a shared LAN
|
||||
segment, or a Bluetooth radio range. v0.3.0 introduces a Nostr-based
|
||||
overlay-discovery channel that lets nodes find each other through any
|
||||
public Nostr relay set, plus a STUN-assisted UDP hole-punching path
|
||||
that connects peers across most consumer NATs.
|
||||
|
||||
Each participating node publishes a signed overlay advert as a Nostr
|
||||
**Kind 37195** parameterized replaceable event. (The kind sits in the
|
||||
application-defined replaceable range and the digits visually spell
|
||||
*FIPS*: 7=F, 1=I, 9=P, 5=S.) The advert lists reachable transport
|
||||
endpoints (UDP, TCP, Tor) and is consumed by other nodes to populate
|
||||
fallback addresses for `via_nostr` peers. Under `policy: open`, the
|
||||
advert cache is also dialed for non-configured peers within a budget
|
||||
cap.
|
||||
|
||||
When both peers are behind NAT, the daemon coordinates a UDP hole
|
||||
punch using NIP-59 gift-wrap signaling for the offer/answer exchange
|
||||
and STUN for reflexive address discovery. A candidate-pair punch
|
||||
planner attempts LAN-private and reflexive paths in parallel; on
|
||||
success the live socket is handed into the standard FIPS UDP transport
|
||||
via a bootstrap-handoff API.
|
||||
|
||||
Operators turn this on with `node.discovery.nostr.enabled: true` and
|
||||
the configured relay set. `policy: open` adds best-effort dialing of
|
||||
non-configured peers seen on the relays. New `peers[].via_nostr` and
|
||||
per-transport `advertise_on_nostr` / `public` flags control what each
|
||||
endpoint contributes to the published advert. Cross-field validation
|
||||
runs at startup to catch mis-configured combinations early.
|
||||
|
||||
A Docker NAT lab covering cone, symmetric, and LAN scenarios is wired
|
||||
into the integration CI matrix. A daemon-side failure-suppression
|
||||
layer (per-npub cooldown after consecutive failures, ±60s clock-skew
|
||||
tolerance, rate-limited WARN logs) keeps relay traffic well-mannered
|
||||
when peers come and go from the open discovery cache. A separate
|
||||
structural cooldown (`protocol_mismatch_cooldown_secs`, default 24h)
|
||||
suppresses retraversal when a punched peer turns out to be running an
|
||||
FMP version this daemon cannot handshake with: the punch completes at
|
||||
the UDP layer, the rx loop spots the version-mismatched packet,
|
||||
reverse-maps to the originating npub, and removes the peer from the
|
||||
next sweep until either side upgrades.
|
||||
|
||||
The auto-connect retry loop pins itself to relay ground truth. Each
|
||||
retry attempt refetches the cached overlay advert against the
|
||||
configured `advert_relays` (one filter query, 2s timeout) before
|
||||
dialing, so a peer whose NAT rebound to a fresh endpoint is recovered
|
||||
on the next retry rather than looping on a stale cached address.
|
||||
`NoTransportForType` triggers a fire-and-forget re-fetch that either
|
||||
replaces or evicts the cache entry. A startup peer-init failure (no
|
||||
operational transport, all addresses unreachable) now schedules a
|
||||
retry instead of leaving the peer in a dead state until the daemon is
|
||||
restarted. Adopted NAT-traversed UDP transports inherit the operator's
|
||||
primary `[transports.udp]` listener config (MTU, recv/send buffer
|
||||
sizes) instead of falling back to the 1280 IPv6-minimum default.
|
||||
|
||||
### Cross-platform reach
|
||||
|
||||
FIPS now ships first-class binaries for **Linux, macOS, Windows, and
|
||||
OpenWrt**.
|
||||
|
||||
- **macOS** support uses the native `utun` TUN interface, raw
|
||||
Ethernet via BPF, a `.pkg` installer with a launchd plist and
|
||||
uninstall script, and an x86_64 cross-compile from arm64 build
|
||||
hosts. A new CI matrix entry runs build and unit-test jobs on
|
||||
macOS hosts.
|
||||
- **Windows** support uses [wintun](https://www.wintun.net/) for the
|
||||
TUN device, a TCP control socket on `localhost:21210` (replacing
|
||||
the Unix domain socket Linux and macOS use), Windows Service
|
||||
lifecycle (`fips.exe --install-service`, `--uninstall-service`,
|
||||
`--service`), and a ZIP package with PowerShell install/uninstall
|
||||
scripts.
|
||||
- **MIPS** atomic-ABI portability lets the daemon build for 32-bit
|
||||
MIPS targets (`mips`, `mipsel`, MIPS32r2) by routing through
|
||||
`portable_atomic`. This unblocks OpenWrt deployments on
|
||||
consumer-grade MIPS routers.
|
||||
- **OpenWrt** packaging gets a procd init with dnsmasq forwarding,
|
||||
proxy NDP, RA route advertisements, and IPv6 forwarding sysctls.
|
||||
The `fips-gateway` is enabled by default in the OpenWrt build.
|
||||
|
||||
### FIPS gateway
|
||||
|
||||
The new `fips-gateway` binary lets unmodified LAN hosts reach FIPS
|
||||
mesh destinations without running the FIPS daemon themselves. Two
|
||||
flows ship together:
|
||||
|
||||
- **Outbound (LAN -> mesh)**: a virtual-IP pool (default
|
||||
`fd01::/112`) is allocated on demand from `.fips`-name DNS lookups.
|
||||
A state-machine lifecycle, conntrack-backed session tracking, proxy
|
||||
NDP on the LAN interface, and TTL-based reclamation handle the
|
||||
bookkeeping. A LAN host that resolves `peer.fips` gets a virtual
|
||||
address it can reach over IP, and the gateway translates the flow
|
||||
to the mesh.
|
||||
- **Inbound (mesh -> LAN)**: new `gateway.port_forwards` config
|
||||
installs prerouting DNAT rules so mesh peers can reach a configured
|
||||
`host:port` on the gateway's LAN. A LAN-side masquerade is added
|
||||
automatically when any forwards are configured, so replies flow
|
||||
back through conntrack.
|
||||
|
||||
A dedicated control socket at `/run/fips/gateway.sock` exposes
|
||||
`show_gateway` and `show_mappings`. `fipstop` adds a Gateway tab with
|
||||
a pool gauge and mappings table.
|
||||
|
||||
The gateway's `dns.listen` source default is now `[::1]:5353`,
|
||||
matching the canonical deployment model: the gateway sits on a host
|
||||
already serving DHCP and DNS to a LAN segment (an OpenWrt AP, a Linux
|
||||
router), port 53 there is taken by the existing resolver, and `.fips`
|
||||
queries are forwarded to the gateway over loopback. The OpenWrt ipk
|
||||
previously overrode the prior `[::]:53` source default in its packaged
|
||||
config; that override is now redundant and has been dropped.
|
||||
Operators on a host without a pre-existing resolver on port 53 can
|
||||
opt back into the wildcard bind by setting `dns.listen: "[::]:53"`
|
||||
explicitly. The new default binds IPv6 loopback only, so forwarders
|
||||
that reach the gateway over IPv4 loopback need an explicit IPv4
|
||||
listen address.
|
||||
|
||||
The cold-boot startup race between `fips.service` and
|
||||
`fips-gateway.service` is handled by a systemd `After=fips.service`
|
||||
ordering, an `ExecStartPre` poll loop that waits up to 30 seconds for
|
||||
the `fips0` interface to appear, and a DNS upstream probe in the
|
||||
gateway itself that retries up to 5 times with 1-second backoff.
|
||||
|
||||
Packaging covers systemd, Debian, AUR, and OpenWrt. The full design
|
||||
is in [`docs/design/fips-gateway.md`](../design/fips-gateway.md).
|
||||
|
||||
### Mesh-interface security baseline
|
||||
|
||||
The FIPS mesh is a flat layer-3 segment. Every authenticated peer can
|
||||
route packets to every other peer's `fips0` address. Peer identity is
|
||||
authenticated end-to-end by the FMP and FSP Noise handshakes, but
|
||||
identity is not authorization. A service on a mesh host that binds to
|
||||
a wildcard address is, by default, reachable from every peer in the
|
||||
mesh.
|
||||
|
||||
v0.3.0 ships an opt-in default-deny baseline that closes this gap on
|
||||
Linux:
|
||||
|
||||
- **`/etc/fips/fips.nft`** is installed as a documented operator
|
||||
conffile. It defines a single `inet fips` nftables table with one
|
||||
chain hooked at `input`, default-denies inbound traffic on
|
||||
`fips0`, and is a no-op for every other interface.
|
||||
- **`fips-firewall.service`** loads it. The unit ships **disabled by
|
||||
default**; activation is an explicit
|
||||
`systemctl enable --now fips-firewall.service`.
|
||||
- Per-service allowances live in **`/etc/fips/fips.d/*.nft`**
|
||||
drop-ins that the baseline includes.
|
||||
|
||||
Choosing opt-in keeps the mesh quick to bring up for evaluation while
|
||||
giving operators a documented, packaged path to lock it down for
|
||||
production. The full design (threat model, rule layout, conntrack
|
||||
handling, drop-in mechanism, and the rationale for a conffile rather
|
||||
than an auto-loaded package side-effect) is in
|
||||
[`docs/design/fips-security.md`](../design/fips-security.md).
|
||||
|
||||
`fipstop`'s Node tab gains a **"Listening on fips0" panel** that
|
||||
surfaces the answer to the operational question "what services on
|
||||
this host are reachable from the mesh, and what does the firewall
|
||||
currently say about each of them?" The panel lists every IPv6
|
||||
listening socket bound to either the wildcard address or this node's
|
||||
`fd00::/8` address, paired with its classification against the
|
||||
running `inet fips` baseline chain: `OPEN` (canonical accept rule),
|
||||
`filt` (falls through to drop), or `filt?` (referenced with matchers
|
||||
the panel cannot fully decompose, e.g. saddr filters or jumps). When
|
||||
`fips-firewall.service` is inactive, a yellow banner above the table
|
||||
reminds the operator that every listener is mesh-exposed; wildcard
|
||||
binds carry a trailing `*` in the Process column. The classifier is
|
||||
built on a new `show_listening_sockets` control query (Linux-only),
|
||||
which is also useful from `fipsctl` for scripting.
|
||||
|
||||
### Peer access control
|
||||
|
||||
Operators can now restrict which mesh peers a node will form direct
|
||||
links with. Optional `/etc/fips/peers.allow` and `/etc/fips/peers.deny`
|
||||
files (TCP-Wrappers style) match against npub, hex pubkey, host
|
||||
alias, or `ALL`. Enforcement runs at three points:
|
||||
|
||||
1. Outbound connect (before dialing).
|
||||
2. Inbound msg1 (the first FMP handshake message from a new peer).
|
||||
3. Outbound msg2 (the response).
|
||||
|
||||
Files reload automatically on mtime change; a new `fipsctl acl show`
|
||||
query reports the effective rule set. A six-node Docker integration
|
||||
harness (`testing/acl/`) exercises allowlist and denylist patterns
|
||||
end-to-end.
|
||||
|
||||
**Important scope distinction**: peer ACLs are an FMP-layer
|
||||
restriction. They control who can establish a *direct link* with this
|
||||
node. They do **not** control session-layer (FSP) reachability through
|
||||
the mesh. A node that denies peer X with an ACL can still receive FSP
|
||||
traffic from X relayed via other peers.
|
||||
|
||||
### Bluetooth Low Energy transport (experimental, Linux)
|
||||
|
||||
A new BLE L2CAP Connection-Oriented Channel transport lets FIPS nodes
|
||||
peer over Bluetooth Low Energy without any IP infrastructure in
|
||||
between. The transport handles per-link MTU negotiation, continuous
|
||||
scan/probe peer discovery with cooldown-based deduplication,
|
||||
continuous advertising, deterministic NodeAddr cross-probe
|
||||
tie-breaker, and a configurable connection pool with eviction.
|
||||
|
||||
This transport is **experimental in v0.3.0**. It is implemented and
|
||||
functional on Linux (BlueZ via `bluer`), but the reliability follow-up
|
||||
logic (probe cooldown, cross-probe tie-breaker, pubkey timeout,
|
||||
continuous advertising semantics, probe-promotion, fail-fast send) is
|
||||
not yet behaviorally tested in CI. Its maturity path is field-driven;
|
||||
please file issues with field reports. macOS BLE support is in
|
||||
development as a separate track and is not part of v0.3.0.
|
||||
|
||||
### UDP transport profiles
|
||||
|
||||
The UDP transport gains posture flags organized around deployment
|
||||
patterns:
|
||||
|
||||
- **Public-facing inbound nodes**: `bind_addr: "0.0.0.0:2121"`,
|
||||
`accept_connections: true` (default), `public: true` for advert
|
||||
publication. v0.3.0 adds STUN-based public-IP autodiscovery so
|
||||
cloud nodes (AWS EIP, GCP, Azure 1:1 NAT) advertise the right
|
||||
address even when the public IP isn't on a host interface.
|
||||
- **Ephemeral leaf nodes**: `outbound_only: true` binds an ephemeral
|
||||
port (`0.0.0.0:0`), refuses inbound msg1, and is never advertised
|
||||
on Nostr regardless of `advertise_on_nostr`. Use this for client
|
||||
postures that should connect outbound only, without exposing an
|
||||
inbound listener on a known port.
|
||||
- **General-purpose nodes**: `accept_connections: false` mirrors the
|
||||
Ethernet/BLE knob without changing the bind address. The Node-level
|
||||
handshake gate carves out msg1 from peers already established on
|
||||
this transport so rekey continues to work.
|
||||
|
||||
Startup validation now rejects `bind_addr` set to a loopback address
|
||||
when at least one peer has a non-loopback UDP address, closing a
|
||||
silent-failure trap from v0.2.0 where Linux's source-address routing
|
||||
check would drop outbound flows from the loopback-bound socket.
|
||||
|
||||
A new `external_addr` field on `transports.udp.*` and
|
||||
`transports.tcp.*` lets operators specify the advertise-as address
|
||||
explicitly. This is useful for UDP as a deterministic alternative to
|
||||
STUN, and required for TCP on cloud-NAT setups (where binding to the
|
||||
public IP fails with `EADDRNOTAVAIL` because the IP isn't on a host
|
||||
interface).
|
||||
|
||||
### `.fips` DNS resolver overhaul
|
||||
|
||||
The IPv6 adapter's `.fips` name resolution has been rebuilt around
|
||||
the constraints of contemporary systemd-based hosts. The default
|
||||
`dns.bind_addr` is now `::1` (IPv6 loopback), and a setup script
|
||||
picks one of five backends in priority order:
|
||||
|
||||
1. systemd-resolved global drop-in
|
||||
(`/etc/systemd/resolved.conf.d/fips.conf`).
|
||||
2. systemd dns-delegate (per-link configuration handed off to
|
||||
systemd-resolved).
|
||||
3. `resolvectl` per-link configuration.
|
||||
4. Standalone `dnsmasq`.
|
||||
5. NetworkManager's dnsmasq plugin.
|
||||
|
||||
Teardown reverses only what setup applied, recorded in a state file
|
||||
at `/run/fips/dns-backend`. A new `testing/dns-resolver/` harness
|
||||
exercises every backend across Debian 12, Debian 13, Ubuntu 22.04,
|
||||
Ubuntu 24.04, and Ubuntu 26.04, so a regression in any of the five
|
||||
backends shows up in CI rather than in the field.
|
||||
|
||||
This overhaul resolves the long-standing silent-drop case where the
|
||||
`resolvectl dns fips0 [<fips0_addr>]:5354` target collided with the
|
||||
daemon's mesh-interface filter on certain systemd-resolved
|
||||
deployments (typically Ubuntu 22 with systemd 249's interface-scoped
|
||||
routing).
|
||||
|
||||
### Operator tooling additions
|
||||
|
||||
A handful of additions land in `fipsctl`, `fipstop`, and the daemon's
|
||||
configuration surface:
|
||||
|
||||
- **`node.log_level`** config field replaces the hardcoded
|
||||
`RUST_LOG=info` previously baked into systemd units and the
|
||||
OpenWrt procd init. The daemon now loads config before
|
||||
initializing tracing so the configured level takes effect.
|
||||
`RUST_LOG` still overrides when set.
|
||||
- **`fipsctl show identity-cache`** is a new query that lists every
|
||||
cached node identity (npub, IPv6 address, display name, LRU age)
|
||||
alongside the configured cache capacity.
|
||||
- **`fipsctl show peers / sessions / cache / routing`** are
|
||||
substantially extended: per-peer security signals (replay
|
||||
suppression count, consecutive decrypt failures), Noise session
|
||||
counters, session indices, rekey lifecycle state, handshake resend
|
||||
counts, K-bit epoch, coords-warmup remaining, drain state, per-peer
|
||||
retry state, per-target lookup detail (attempt, age, last sent),
|
||||
and pending TUN packet queue depth.
|
||||
- **Historical statistics**: in-memory time-series rings on the
|
||||
daemon (1-second × 3600 fast, 1-minute × 1440 slow) cover per-node
|
||||
and per-peer metrics. New `show_stats_*` control-socket queries, a
|
||||
`fipsctl stats list / peers / history` subcommand with Unicode
|
||||
sparkline rendering, and a `fipstop` Graphs tab with btop-style
|
||||
sparklines surface them to the operator.
|
||||
|
||||
### Performance
|
||||
|
||||
Two independent perf threads land in v0.3.0: a session-layer crypto
|
||||
backend swap, and a Linux receive-path overhaul.
|
||||
|
||||
**Session-layer crypto backend.** The ChaCha20-Poly1305 backend used
|
||||
by every FIPS Noise session (end-to-end FSP traffic and link-layer
|
||||
FMP traffic alike) has been swapped from RustCrypto's
|
||||
`chacha20poly1305` crate to `ring 0.17`. ring wraps BoringSSL's
|
||||
hand-tuned ChaCha20-Poly1305 implementation, which dispatches to NEON
|
||||
on aarch64 and AVX2 / AVX-512 on x86_64. Typical throughput is in the
|
||||
3-5 GB/s/core range, versus the ~600-800 MB/s/core RustCrypto soft
|
||||
path on the same hardware.
|
||||
|
||||
Wire format is unchanged. ChaCha20-Poly1305 is byte-deterministic for
|
||||
a given `(key, nonce, plaintext, aad)`, so any correct AEAD
|
||||
implementation produces identical ciphertext. A mixed mesh with some
|
||||
nodes pre-swap and some post-swap interoperates without protocol
|
||||
awareness; v0.3.0 can roll out across a mesh in any order.
|
||||
|
||||
Measurements on an aarch64 Apple Silicon docker target:
|
||||
|
||||
- Two-node TCP single-stream: 437 -> 1097 Mbps (about 2.5×).
|
||||
- Two-node UDP at 1000 Mbit: 599 Mbps with 40% loss -> lossless at
|
||||
line rate.
|
||||
- Three-node ping under bulk-traffic load: 7.68 ms avg / 215 ms max
|
||||
-> 0.72 ms / 3.6 ms max as the relay path stops being crypto-bound.
|
||||
|
||||
No operator-visible action is required; the swap is internal to the
|
||||
session layer.
|
||||
|
||||
**Linux UDP receive path.** The Linux UDP receive path now uses
|
||||
`recvmmsg(2)` with a 32-packet batch in place of single-packet
|
||||
`recvmsg(2)`. A single `readable()` wakeup drains up to 32 datagrams
|
||||
in one syscall before yielding back to the reactor, eliminating the
|
||||
per-packet scheduler-hop and futex cost that previously capped
|
||||
inbound rate at one event per scheduler quantum independent of CPU.
|
||||
`SO_RXQ_OVFL` is sampled once per batch and surfaced through
|
||||
`AsyncUdpSocket::recv_batch` so the existing 1Hz transport-congestion
|
||||
detector continues to feed the per-transport `dropping` flag. macOS
|
||||
and Windows fall through to the per-packet path; `recvmmsg` is
|
||||
Linux-specific.
|
||||
|
||||
**Inner rx-loop drain batching.** `Node::run_rx_loop` drains up to
|
||||
256 additional ready items via `try_recv()` after each
|
||||
`tokio::select!` await fires on the packet and TUN-outbound branches,
|
||||
in a tight inner loop before yielding. Previously the select cost a
|
||||
full scheduler hop and futex per packet, capping throughput at one
|
||||
event per scheduler quantum with the worker near-idle. `biased`
|
||||
ordering keeps data-plane branches priority over tick / control / DNS
|
||||
under sustained load; the 256 cap keeps the worker on a busy stream
|
||||
between yield points (about 400 KB of contiguous traffic) while still
|
||||
bounding the inner loop so a flood on one branch cannot starve the
|
||||
periodic tick or control socket.
|
||||
|
||||
**Eager `pubkey_full` precompute.** `PeerIdentity::pubkey_full()`
|
||||
precomputes the parity-aware full secp256k1 public key at
|
||||
construction in `from_pubkey`. Previously the method fell through to
|
||||
an EC point parse on every call when the full key wasn't passed at
|
||||
construction (i.e. for every peer constructed from an npub or x-only
|
||||
key), about 6% of per-packet CPU on the bulk-data send path for a
|
||||
value that never changed after construction. The same parse already
|
||||
runs at construction inside `NodeAddr::from_pubkey`, so the cost is
|
||||
paid once where it would be paid anyway.
|
||||
|
||||
These three changes are a coordinated set: the syscall batching
|
||||
removes the per-packet kernel cost, the inner-loop drain removes the
|
||||
per-packet scheduler cost, and the pubkey-cache change removes the
|
||||
per-packet crypto-derivation cost. Like the AEAD swap, they are all
|
||||
internal and require no operator action.
|
||||
|
||||
### Examples
|
||||
|
||||
- **macOS WireGuard companion** ([#51](https://github.com/jmcorgan/fips/pull/51)):
|
||||
run FIPS in a local Docker container and route `.fips` traffic
|
||||
from the macOS host through a WireGuard tunnel to the container's
|
||||
`fips0`. Only traffic destined for `fd00::/8` transits the
|
||||
companion; regular internet traffic continues to use the host
|
||||
network. Persistent FIPS and WireGuard key material is generated
|
||||
on first run.
|
||||
|
||||
### Documentation
|
||||
|
||||
- **`docs/design/port-advertisement-and-nat-traversal.md`**
|
||||
documents how nodes find each other through Nostr relays and the
|
||||
STUN-assisted UDP hole punch.
|
||||
- **`docs/design/fips-gateway.md`** documents the gateway's virtual
|
||||
IP pool, lifecycle, control surface, and packaging.
|
||||
- **`docs/design/fips-security.md`** documents the mesh-interface
|
||||
security posture, threat model, default-deny baseline, and drop-in
|
||||
workflow.
|
||||
- **`CONTRIBUTING.md`** has been expanded with build prerequisites,
|
||||
Rust toolchain setup, and first-build steps.
|
||||
|
||||
The `docs/` tree has been reorganized end-to-end into four sections
|
||||
(*tutorials / how-to / reference / design*) with a new
|
||||
[`docs/getting-started.md`](../getting-started.md) and per-section
|
||||
landing pages. Content was reconciled against current source:
|
||||
protocol-layer details, wire-format diagrams, configuration knobs,
|
||||
and CLI references were brought back into agreement with the
|
||||
implementation. See [Documentation pointers](#documentation-pointers)
|
||||
below for entry points by reader intent.
|
||||
|
||||
## Behavior changes worth flagging
|
||||
|
||||
These default-config changes affect every operator on upgrade, even
|
||||
those with no explicit configuration. Two items below — bloom-filter
|
||||
fill-ratio validation and TreeAnnounce ancestry validation — first
|
||||
shipped in v0.2.1 and roll forward into v0.3.0; the rest are
|
||||
v0.3.0-net-new.
|
||||
|
||||
- **Discovery rate-limiting** has been retuned to be less aggressive
|
||||
at cold start. v0.2.0 used a single-lookup-with-internal-retry
|
||||
model where a timed-out lookup during bloom-filter propagation
|
||||
could suppress retries for 30 seconds while none of the reset
|
||||
triggers fired on a stable post-handshake topology. v0.3.0
|
||||
replaces this with a per-attempt timeout sequence
|
||||
(`node.discovery.attempt_timeouts_secs`, default `[1, 2, 4, 8]`,
|
||||
15s total). Each attempt sends a fresh `LookupRequest` with a new
|
||||
`request_id`, letting successive attempts take different
|
||||
forwarding paths as the bloom and tree state evolve. Post-failure
|
||||
suppression is **off by default**; operators with chatty
|
||||
applications can opt back in via `backoff_base_secs` /
|
||||
`backoff_max_secs`.
|
||||
- **MMP report intervals** are retuned for constrained transports.
|
||||
The steady-state floor moves from 100ms to 1000ms, the ceiling
|
||||
from 2000ms to 5000ms, with a cold-start phase running 200ms for
|
||||
the first 5 SRTT samples. This reduces BLE overhead by roughly
|
||||
10× while keeping reports well above the EWMA convergence
|
||||
threshold. Session-layer MMP intervals are unchanged.
|
||||
- **Bloom filter fill-ratio validation** runs on every inbound
|
||||
`FilterAnnounce`. Filters whose derived false-positive rate
|
||||
exceeds `node.bloom.max_inbound_fpr` (default 0.05) are rejected
|
||||
silently on the wire, logged at WARN, and counted in a new
|
||||
`bloom.fill_exceeded` counter. A rate-limited WARN also fires
|
||||
when the local outgoing filter exceeds the cap.
|
||||
- **TreeAnnounce ancestry validation** is now run before tree-state
|
||||
mutation, enforcing ancestry-self-match, root-single-entry,
|
||||
parent-second-entry, and root-is-minimum-NodeAddr. Non-conforming
|
||||
announces are rejected with a WARN. Mixed v0.2.0 / v0.2.1 / v0.3.0
|
||||
meshes may produce WARN log lines on the v0.2.1+ side until all
|
||||
peers upgrade; behavior is correct, log noise only.
|
||||
- **Log noise reduction**: 35 info-level log messages have been
|
||||
demoted to debug (handshake cross-connection mechanics, periodic
|
||||
MMP telemetry, TUN/transport shutdown, retry scheduling). The
|
||||
default `RUST_LOG` in systemd units is now `info`, where it
|
||||
previously ran at `debug`. Operator-visible info output now
|
||||
focuses on lifecycle events, peer promotions, session
|
||||
establishment, parent switches, and transport start/stop.
|
||||
|
||||
## Notable bug fixes
|
||||
|
||||
These pre-existing v0.2.0 bugs are worth singling out because they
|
||||
either affected real-world deployments or produced misleading
|
||||
operator experiences. The CHANGELOG has the exhaustive list; this is
|
||||
the operator-relevant subset. Four items below first shipped in
|
||||
v0.2.1 and roll forward into v0.3.0: auto-connect Disconnect-reconnect,
|
||||
`fipsctl connect` mesh-address rejection, `fd00::/8` routing
|
||||
protection from Tailscale interception, and bloom-filter routing
|
||||
greedy-tree fallback. The control-socket path-detection fix landed
|
||||
in v0.2.1 as well, and the unified resolver below is the v0.3.0
|
||||
refactor that builds on it.
|
||||
|
||||
- **DNS responder silent-drop on systemd-resolved** is fixed: the
|
||||
responder no longer drops queries on Ubuntu 22 / Debian 13 and
|
||||
similar deployments where systemd applies interface-scoped
|
||||
routing. Default bind moves to `::1`; new global drop-in backend
|
||||
available ([#52](https://github.com/jmcorgan/fips/issues/52),
|
||||
[#77](https://github.com/jmcorgan/fips/issues/77)).
|
||||
- **Auto-connect peers reconnect after a graceful Disconnect.**
|
||||
Previously, a clean upstream shutdown left the auto-connect peer
|
||||
orphaned; only the link-dead, decrypt-fail, and peer-restart
|
||||
paths scheduled a reconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **`fipsctl connect` rejects FIPS mesh addresses** (`fd00::/8`)
|
||||
for `udp`, `tcp`, and `ethernet` transports with a clear error
|
||||
message, instead of echoing success while the daemon silently
|
||||
failed the bind with `EAFNOSUPPORT`
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61), reported by
|
||||
[@SwapMarket](https://github.com/SwapMarket)).
|
||||
- **Default control-socket path resolution unified.** Daemon and
|
||||
client tools now share a single resolver, eliminating a divergence
|
||||
where `fipsctl` / `fipstop` could connect to a socket the daemon
|
||||
never bound (notably on dev runs with `XDG_RUNTIME_DIR` set, or
|
||||
after a prior packaged install left a root-owned `/run/fips`
|
||||
behind). Canonical order is
|
||||
`/run/fips` -> `$XDG_RUNTIME_DIR/fips/` -> `/tmp/fips-<name>`. The
|
||||
`/run/fips` arm is selected by directory existence; the kernel
|
||||
enforces actual access at `connect(2)` time, so users not yet in
|
||||
the `fips` group get a clear `EACCES` rather than a silent path
|
||||
mismatch and a misleading `No such file` fallback to
|
||||
`$XDG_RUNTIME_DIR`. `XDG_RUNTIME_DIR` is validated as an existing
|
||||
directory before being used so stale post-logout values are
|
||||
treated as missing. The deployed fleet is unaffected: packaged
|
||||
configs set `node.control.socket_path` explicitly
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30), reported by
|
||||
[@Sebastix](https://github.com/Sebastix)).
|
||||
- **`fd00::/8` routing protected from Tailscale interception.** The
|
||||
daemon installs an IPv6 routing-policy rule
|
||||
(`ip -6 rule to fd00::/8 lookup main priority 5265`) at TUN
|
||||
setup, so Tailscale's table 52 default route can no longer divert
|
||||
mesh traffic.
|
||||
- **TCP-over-FIPS reliability on mixed-MTU paths** is markedly
|
||||
improved. Four interlocking changes ship together:
|
||||
`Node::transport_mtu()` is now deterministic across daemon
|
||||
restarts (min across operational transports rather than
|
||||
insertion-order-dependent); the TCP MSS clamp at the TUN boundary
|
||||
reads per-destination path MTU instead of a single global ceiling;
|
||||
reactive `MtuExceeded` from forwarders is mirrored back into the
|
||||
TUN-side `path_mtu_lookup` so later flows pick up forward-path
|
||||
bottlenecks without re-discovery; and the proactive end-to-end
|
||||
`PathMtuNotification` echoed by the destination is mirrored into
|
||||
the same TUN-side store. Without that fourth piece, on long-lived
|
||||
stable paths where the destination's echo had tightened the
|
||||
session MTU but no transit router had emitted a fresh
|
||||
`MtuExceeded`, new TCP flows opened in that window were clamped by
|
||||
the staler discovery-time value. The proactive mirror uses the
|
||||
same tighter-only semantics as the reactive mirror, so it never
|
||||
loosens the clamp. The Windows TUN reader receives the same
|
||||
per-destination plumbing.
|
||||
- **Bloom filter routing greedy-tree fallback.** `find_next_hop` no
|
||||
longer returns `NoRoute` when the bloom candidate set is non-empty
|
||||
but no candidate is strictly closer than the current node; it
|
||||
falls through to greedy tree routing instead. Previously, this
|
||||
caused dropped packets in topologies where the tree parent was
|
||||
closer but not a bloom candidate.
|
||||
- **`fipstop` graceful tty-init failure.** `ratatui::try_init()`
|
||||
produces a clean error message instead of a hard crash when
|
||||
terminal initialization fails (Docker on macOS Sequoia, ttyless
|
||||
environments).
|
||||
- **TreeAnnounce ancestry on self-root transitions.** When a node
|
||||
had no smaller-NodeAddr peer to use as a parent, the spanning-tree
|
||||
state correctly promoted it to root, but the ancestry advertised
|
||||
on the next `TreeAnnounce` still referenced its previous parent's
|
||||
path. Receiving peers rejected the announce as
|
||||
`invalid ancestry: advertised root X is not the minimum path entry
|
||||
Y`, blocking mesh transit on any path that needed to traverse the
|
||||
node. The self-root transition is now detected explicitly in
|
||||
`TreeState::become_root` and the advertised ancestry rebuilt to
|
||||
start from self; the MMP receive handler corrects stale ancestry
|
||||
inherited across reconnect eagerly rather than waiting for the
|
||||
next observation tick.
|
||||
- **Spanning-tree internal-path updates** that change only the
|
||||
internal path between root and leaf (without changing the root or
|
||||
the depth) now propagate to leaves correctly. Previously, a leaf
|
||||
could continue routing against a stale internal path until the
|
||||
parent or depth also changed.
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
Operator-actionable items when moving from v0.2.x to v0.3.0:
|
||||
|
||||
- **Control socket JSON schema (breaking, pre-1.0).**
|
||||
- `show_cache` response field `entries` has changed type from a
|
||||
`u64` count to an array of entry objects. The previous scalar
|
||||
value is now in a new `count` field.
|
||||
- `show_routing` response field `pending_lookups` has changed
|
||||
type from a `u64` count to an array of per-target lookup
|
||||
objects.
|
||||
- External tooling parsing these fields as numbers must be
|
||||
updated. In-tree `fipstop` is adjusted to the new schema. The
|
||||
control-socket interface remains pre-1.0 and is not covered by
|
||||
stability guarantees.
|
||||
|
||||
- **Cargo feature flags removed.** `tui`, `ble`, `gateway`, and
|
||||
`nostr-discovery` are gone. Subsystem inclusion is now driven by
|
||||
platform `cfg` gates, so plain `cargo build` compiles everything
|
||||
available on the target without `--features` invocations.
|
||||
Source-build tooling that passed any of these features should be
|
||||
updated to omit them.
|
||||
|
||||
- **Discovery rate-limiting defaults changed.** Post-failure
|
||||
suppression is **off by default**
|
||||
(`node.discovery.backoff_base_secs: 0`, `backoff_max_secs: 0`).
|
||||
Operators relying on the prior 30s base / 300s cap behavior must
|
||||
set those fields explicitly. The per-attempt sequence
|
||||
(`attempt_timeouts_secs`, default `[1, 2, 4, 8]`) now governs
|
||||
cold-start lookup behavior.
|
||||
|
||||
- **`.fips` DNS bind address default changed.** The default
|
||||
`dns.bind_addr` is now `::1`. Operators with explicit overrides
|
||||
of this field should review them; many existing overrides were
|
||||
workarounds for the silent-drop bug that this release fixes
|
||||
properly.
|
||||
|
||||
- **Gateway `dns.listen` source default changed.** The
|
||||
`fips-gateway` `dns.listen` default is now `[::1]:5353` (was
|
||||
`[::]:53`), matching the canonical deployment model where a
|
||||
pre-existing resolver on the host already owns port 53. The
|
||||
OpenWrt ipk previously overrode this in its packaged config; the
|
||||
override is now redundant and has been dropped. Operators on a
|
||||
host without a pre-existing resolver on port 53 can opt back into
|
||||
the wildcard bind by setting `dns.listen: "[::]:53"` explicitly.
|
||||
The new default binds IPv6 loopback only, so forwarders that
|
||||
reach the gateway over IPv4 loopback need an explicit IPv4 listen
|
||||
address.
|
||||
|
||||
- **systemd unit log level.** The shipped systemd units no longer
|
||||
hardcode `RUST_LOG=info`; the daemon's effective log level is
|
||||
driven by `node.log_level` (default `info`). `RUST_LOG`, when
|
||||
set, still overrides.
|
||||
|
||||
- **UDP transport `bind_addr` validation.** Startup now rejects a
|
||||
`bind_addr` set to a loopback address when at least one peer has
|
||||
a non-loopback UDP address. Operators who configured a loopback
|
||||
UDP bind as a workaround should switch to `outbound_only: true`
|
||||
for the same effect, plus the correct semantics (kernel-assigned
|
||||
ephemeral port, refuses inbound, never advertised).
|
||||
|
||||
- **Tor advert port.** If the Tor `HiddenServicePort` virtual port
|
||||
isn't 443, set `transports.tor.advertised_port` to match. The
|
||||
default is 443 and matches the conventional virtual-port choice.
|
||||
|
||||
## Documentation pointers
|
||||
|
||||
v0.3.0 ships a `docs/` tree reorganized into four sections
|
||||
(*tutorials / how-to / reference / design*). A new top-level
|
||||
[`docs/getting-started.md`](../getting-started.md) and per-section
|
||||
landing pages anchor the entry points.
|
||||
|
||||
Entry points by reader intent:
|
||||
|
||||
- **New users**: [`docs/getting-started.md`](../getting-started.md)
|
||||
and [`docs/tutorials/`](../tutorials/) cover guided introductions
|
||||
for bringing up your first node, joining the test mesh,
|
||||
advertising a node over Nostr, hosting a service, deploying a
|
||||
gateway, walking through the IPv6 adapter, and resolving peers
|
||||
via Nostr.
|
||||
- **Operators with a specific task**:
|
||||
[`docs/how-to/`](../how-to/) holds task-driven guides for enabling
|
||||
Nostr discovery, deploying the gateway, troubleshooting the
|
||||
gateway, deploying a Tor onion, hosting aliases, persistent
|
||||
identity, running unprivileged, setting up a Bluetooth peer,
|
||||
enabling the mesh firewall, tuning UDP buffers, and diagnosing
|
||||
MTU issues.
|
||||
- **Reference lookups**: [`docs/reference/`](../reference/) holds
|
||||
the config field reference, control-socket query reference, the
|
||||
`fips`, `fipsctl`, `fipstop`, and `fips-gateway` CLI references,
|
||||
and the protocol diagram set.
|
||||
- **Architectural background**: [`docs/design/`](../design/) holds
|
||||
design rationale for FIPS as a whole, FMP and FSP, the spanning
|
||||
tree, bloom-filter discovery, transports, the IPv6 adapter, the
|
||||
Nostr discovery layer, and the gateway.
|
||||
- **Security**: [`docs/design/fips-security.md`](../design/fips-security.md)
|
||||
documents the mesh-interface security baseline, threat model, and
|
||||
drop-in workflow.
|
||||
|
||||
## Getting v0.3.0
|
||||
|
||||
- **Linux x86_64 / aarch64**: `.deb` and tarball at the
|
||||
[v0.3.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.3.0).
|
||||
- **Arch Linux**: `fips` from the AUR.
|
||||
- **macOS**: `.pkg` at the v0.3.0 release page.
|
||||
- **Windows**: ZIP at the v0.3.0 release page.
|
||||
- **OpenWrt**: `.ipk` at the v0.3.0 release page.
|
||||
- **From source**: `cargo build --release` from a checkout of the
|
||||
v0.3.0 tag.
|
||||
|
||||
The full per-commit changelog lives in
|
||||
[`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at
|
||||
[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips).
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed code, packaging work, bug reports,
|
||||
or reviews to this release.
|
||||
|
||||
**Code and packaging**:
|
||||
|
||||
- [@jcorgan](https://github.com/jmcorgan): release shepherd, Nostr
|
||||
discovery / NAT traversal, `fips-gateway`, ACL infrastructure,
|
||||
packaging, security baseline, BLE follow-ups.
|
||||
- [@Origami74](https://github.com/Origami74): macOS platform support,
|
||||
from-source Docker companion build and `fipstop` terminal-init
|
||||
handling, gateway co-development, OpenWrt BLE-feature build fix,
|
||||
AUR-workflow follow-ups.
|
||||
- [@jodobear](https://github.com/jodobear): Linux release-artifact
|
||||
workflow and target-aware build scripts, CONTRIBUTING.md
|
||||
expansion, rekey integration-test stabilization.
|
||||
- [@tidley](https://github.com/tidley): Nostr-mediated overlay
|
||||
discovery and UDP NAT traversal
|
||||
([#53](https://github.com/jmcorgan/fips/pull/53)).
|
||||
- [@alexxie16](https://github.com/alexxie16): peer ACL enforcement
|
||||
([#50](https://github.com/jmcorgan/fips/pull/50)),
|
||||
macOS WireGuard companion example
|
||||
([#51](https://github.com/jmcorgan/fips/pull/51)),
|
||||
follow-up ([#67](https://github.com/jmcorgan/fips/pull/67)).
|
||||
- [@osh](https://github.com/osh): diagnostic queries for security
|
||||
validation and mesh debugging
|
||||
([#42](https://github.com/jmcorgan/fips/pull/42)).
|
||||
- [@OceanSlim](https://github.com/0ceanSlim): Windows platform
|
||||
support ([#45](https://github.com/jmcorgan/fips/pull/45)).
|
||||
- [@mmalmi](https://github.com/mmalmi): ring AEAD backend
|
||||
([#80](https://github.com/jmcorgan/fips/pull/80)),
|
||||
hot-path drain batching + recvmmsg + eager pubkey_full
|
||||
([#81](https://github.com/jmcorgan/fips/pull/81)),
|
||||
TreeAnnounce self-root ancestry + overlay-advert retry hygiene
|
||||
([#82](https://github.com/jmcorgan/fips/pull/82)),
|
||||
NAT-traversal MTU inheritance
|
||||
([#83](https://github.com/jmcorgan/fips/pull/83)).
|
||||
- [@dskvr](https://github.com/dskvr): initial Arch Linux AUR
|
||||
packaging ([#21](https://github.com/jmcorgan/fips/pull/21)) and
|
||||
the AUR publish workflow.
|
||||
- [@SatsAndSports](https://github.com/SatsAndSports): rekey
|
||||
message-1 admit fix on non-accepting transports
|
||||
([#49](https://github.com/jmcorgan/fips/pull/49)),
|
||||
TreeAnnounce semantic validation, gateway test image fix
|
||||
([#69](https://github.com/jmcorgan/fips/pull/69)).
|
||||
- [@andrewheadricke](https://github.com/andrewheadricke): MIPS
|
||||
atomic-ABI portability via `portable_atomic`
|
||||
([#62](https://github.com/jmcorgan/fips/pull/62)).
|
||||
- [@sh1ftred](https://github.com/sh1ftred): Arch packaging namcap
|
||||
fixes ([#63](https://github.com/jmcorgan/fips/pull/63)).
|
||||
- [@oleksky](https://github.com/oleksky): macOS WireGuard companion
|
||||
collaboration on [#51](https://github.com/jmcorgan/fips/pull/51).
|
||||
|
||||
**Issue reports that drove fixes in this release**:
|
||||
|
||||
- [@deavmi](https://github.com/deavmi): MIPS daemon build support
|
||||
([#26](https://github.com/jmcorgan/fips/issues/26)).
|
||||
- [@Sebastix](https://github.com/Sebastix): fipsctl/fipstop
|
||||
control-socket path detection
|
||||
([#30](https://github.com/jmcorgan/fips/issues/30)).
|
||||
- [@SwapMarket](https://github.com/SwapMarket): auto-connect
|
||||
reconnect after graceful disconnect
|
||||
([#60](https://github.com/jmcorgan/fips/issues/60)) and
|
||||
fipsctl mesh-address rejection
|
||||
([#61](https://github.com/jmcorgan/fips/issues/61)).
|
||||
@@ -96,7 +96,7 @@ You should currently have:
|
||||
```
|
||||
|
||||
Expect `test-us01` listed with `connectivity` active and a
|
||||
`transport_addr` of roughly `217.77.8.91:2121`.
|
||||
`transport_addr` of roughly `test-us01.fips.network:2121`.
|
||||
|
||||
If either of those isn't true, finish the previous two
|
||||
tutorials first; the Nostr discovery layer is built on top of
|
||||
@@ -189,7 +189,7 @@ sudo fipsctl show peers
|
||||
|
||||
`test-us01` should appear with `connectivity` active and a
|
||||
`transport_addr` reflecting the address that was resolved from
|
||||
the advert — `217.77.8.91:2121` at time of writing. That field
|
||||
the advert — `test-us01.fips.network:2121` at time of writing. That field
|
||||
is the strong signal: nothing in your config gave the daemon
|
||||
that IP, yet there it is.
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ FIPS_NSEC=
|
||||
|
||||
# Peer configuration (leave empty for standalone operation)
|
||||
FIPS_PEER_NPUB=npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98
|
||||
FIPS_PEER_ADDR=217.77.8.91:2121
|
||||
FIPS_PEER_ADDR=test-us01.fips.network:2121
|
||||
FIPS_PEER_ALIAS=vps
|
||||
FIPS_PEER_TRANSPORT=udp
|
||||
|
||||
@@ -32,5 +32,5 @@ peers:
|
||||
alias: "fips-test-node"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "217.77.8.91:2121"
|
||||
addr: "test-us01.fips.network:2121"
|
||||
connect_policy: auto_connect
|
||||
|
||||
@@ -116,7 +116,7 @@ peers: []
|
||||
# via_nostr: true
|
||||
# addresses:
|
||||
# - transport: udp
|
||||
# addr: "217.77.8.91:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
# addr: "test-us01.fips.network:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
# - transport: udp
|
||||
# addr: "nat" # Use node.discovery.nostr for Nostr/STUN hole punching
|
||||
# connect_policy: auto_connect
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
# FIPS Node Configuration
|
||||
#
|
||||
# Edit this file before starting the FIPS daemon.
|
||||
# Full reference: https://github.com/jmcorgan/fips/blob/master/docs/reference/configuration.md
|
||||
#
|
||||
# After editing, restart the daemon:
|
||||
# /etc/init.d/fips restart
|
||||
|
||||
node:
|
||||
identity:
|
||||
# A persistent keypair is generated on first start and saved to
|
||||
# /etc/fips/fips.key (private) and /etc/fips/fips.pub (public).
|
||||
# The router keeps the same FIPS identity across reboots.
|
||||
# By default, a new ephemeral keypair is generated on each start.
|
||||
# Uncomment persistent to keep the same identity across restarts;
|
||||
# on first start a keypair is saved to fips.key/fips.pub next to
|
||||
# this config file (mode 0600/0644).
|
||||
# persistent: true
|
||||
#
|
||||
# To use an explicit key instead, comment out 'persistent' and set:
|
||||
# nsec: "your-64-char-hex-private-key"
|
||||
persistent: true
|
||||
# Or set an explicit key (overrides persistent):
|
||||
# nsec: "nsec1..."
|
||||
discovery:
|
||||
# Optional Nostr-mediated overlay endpoint discovery.
|
||||
# nostr:
|
||||
# enabled: true
|
||||
# policy: configured_only # disabled | configured_only | open
|
||||
# open_discovery_max_pending: 64 # caps queued open-discovery retries
|
||||
# app: "fips-overlay-v1"
|
||||
# advertise: true
|
||||
# advert_relays:
|
||||
# - "wss://relay.damus.io"
|
||||
# - "wss://nos.lol"
|
||||
# - "wss://offchain.pub"
|
||||
# dm_relays:
|
||||
# - "wss://relay.damus.io"
|
||||
# - "wss://nos.lol"
|
||||
# - "wss://offchain.pub"
|
||||
# # Optional override. If omitted, FIPS uses the built-in STUN list.
|
||||
# # Built-in relay/STUN defaults are best-effort and should be
|
||||
# # overridden by operators for production use.
|
||||
# stun_servers:
|
||||
# - "stun:stun.l.google.com:19302"
|
||||
# - "stun:stun.cloudflare.com:3478"
|
||||
# - "stun:global.stun.twilio.com:3478"
|
||||
|
||||
tun:
|
||||
enabled: true
|
||||
@@ -23,68 +41,85 @@ tun:
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
# bind_addr defaults to "::1" (IPv6 loopback). On OpenWrt, dnsmasq
|
||||
# owns port 53; fips-dns-setup adds a forwarder rule pointing at
|
||||
# 127.0.0.1#5354. Loopback bind is sufficient for that path.
|
||||
# bind_addr defaults to "::1" (IPv6 loopback). The shipped
|
||||
# fips-dns-setup script configures systemd-resolved with a global
|
||||
# /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at
|
||||
# [::1]:5354.
|
||||
#
|
||||
# Set "::" to expose the responder to LAN clients directly (rare —
|
||||
# most installs let dnsmasq forward instead). The mesh-interface
|
||||
# filter in src/upper/dns.rs defends /etc/fips/hosts aliases from
|
||||
# cross-mesh enumeration when bound to a wildcard.
|
||||
# Set "::" to expose the responder to mesh peers as well (e.g. for
|
||||
# gateway hosts that resolve .fips on behalf of LAN clients). The
|
||||
# mesh-interface filter in src/upper/dns.rs will still defend
|
||||
# /etc/fips/hosts aliases from cross-mesh enumeration.
|
||||
# bind_addr: "::1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
udp:
|
||||
bind_addr: "0.0.0.0:2121"
|
||||
# advertise_on_nostr: true
|
||||
# public: false # false => advertise udp:nat; true => advertise bound host:port
|
||||
# accept_connections: true # default; refuse inbound msg1 when false
|
||||
# outbound_only: false # true => bind ephemeral, no listener on a
|
||||
# # known port. Forces advertise_on_nostr=false
|
||||
# # and accept_connections=false. Pure-client
|
||||
# # posture; bind_addr is ignored.
|
||||
|
||||
# Ethernet transport — uncomment and set your interface names.
|
||||
# Use physical port names, NOT bridge names (e.g. eth1, not br-lan).
|
||||
tcp:
|
||||
# Accepts inbound connections. No static outbound peers.
|
||||
bind_addr: "0.0.0.0:8443"
|
||||
# advertise_on_nostr: true
|
||||
|
||||
# Ethernet transport — physical port names, NOT bridge names.
|
||||
# Run 'ip link show' on the router to identify port names.
|
||||
# See: https://github.com/jmcorgan/fips/blob/master/deploy/native/README.md
|
||||
|
||||
ethernet:
|
||||
wan:
|
||||
interface: "eth0" # WAN
|
||||
interface: "eth0"
|
||||
discovery: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
wwan:
|
||||
interface: "phy0-sta0" # WWAN
|
||||
interface: "phy0-sta0"
|
||||
discovery: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
lan:
|
||||
interface: "br-lan" # LAN
|
||||
interface: "br-lan"
|
||||
discovery: true
|
||||
announce: true
|
||||
auto_connect: true
|
||||
accept_connections: true
|
||||
|
||||
peers: []
|
||||
# Static peers for bootstrapping (add at least one if using UDP-only):
|
||||
# - npub: "npub1..."
|
||||
# alias: "gateway"
|
||||
# addresses:
|
||||
# - transport: udp
|
||||
# addr: "1.2.3.4:2121" # IP or hostname
|
||||
|
||||
# --- Outbound LAN gateway ---
|
||||
#
|
||||
# Allows unmodified LAN hosts to reach FIPS mesh destinations via
|
||||
# DNS-allocated virtual IPs and kernel nftables NAT.
|
||||
#
|
||||
# The gateway DNS listens on `[::1]:5353` by default (set in source) so it
|
||||
# does not conflict with dnsmasq on port 53. The init script configures
|
||||
# dnsmasq to forward .fips queries to the gateway automatically.
|
||||
# Bluetooth Low Energy transport — requires BlueZ and the 'ble' feature.
|
||||
# ble:
|
||||
# adapter: "hci0"
|
||||
# mtu: 2048
|
||||
# advertise: true
|
||||
# scan: true
|
||||
# auto_connect: true
|
||||
# accept_connections: true
|
||||
|
||||
# Outbound LAN gateway. dnsmasq forwards .fips queries to listen=[::1]:5353
|
||||
# (configured by the fips init script). Requires IPv6 forwarding enabled.
|
||||
gateway:
|
||||
enabled: true
|
||||
pool: "fd01::/112" # virtual IP range (up to 65535 addresses)
|
||||
lan_interface: "br-lan" # LAN-facing interface for proxy NDP
|
||||
pool: "fd01::/112"
|
||||
lan_interface: "br-lan"
|
||||
dns:
|
||||
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
|
||||
ttl: 60 # DNS TTL and mapping lifetime (seconds)
|
||||
pool_grace_period: 60 # seconds after last session before reclaiming
|
||||
listen: "[::1]:5353"
|
||||
upstream: "[::1]:5354"
|
||||
ttl: 60
|
||||
pool_grace_period: 60
|
||||
|
||||
peers: []
|
||||
# Static peers for bootstrapping (UDP or TCP):
|
||||
# - npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
||||
# alias: "gateway"
|
||||
# via_nostr: true
|
||||
# addresses:
|
||||
# - transport: udp
|
||||
# addr: "test-us01.fips.network:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
# - transport: udp
|
||||
# addr: "nat" # Use node.discovery.nostr for Nostr/STUN hole punching
|
||||
# connect_policy: auto_connect
|
||||
|
||||
@@ -109,7 +109,7 @@ peers:
|
||||
alias: "gateway"
|
||||
addresses:
|
||||
- transport: udp
|
||||
addr: "217.77.8.91:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
addr: "test-us01.fips.network:2121" # IP or hostname (e.g., "peer.example.com:2121")
|
||||
connect_policy: auto_connect
|
||||
```
|
||||
|
||||
|
||||
+18
-18
@@ -11,9 +11,9 @@ use serde::{Deserialize, Serialize};
|
||||
/// Parse an `external_addr` config string against a known bind port,
|
||||
/// producing the absolute `SocketAddr` to advertise on Nostr.
|
||||
///
|
||||
/// Accepts either a bare IP (`"54.183.70.180"` or `"[::1]"`) — in which
|
||||
/// Accepts either a bare IP (`"198.51.100.1"` or `"[::1]"`) — in which
|
||||
/// case the bind port is appended — or a full `host:port` form
|
||||
/// (`"54.183.70.180:443"` or `"[::1]:443"`). Returns `None` on any parse
|
||||
/// (`"198.51.100.1:443"` or `"[::1]:443"`). Returns `None` on any parse
|
||||
/// error. IPv6 must use bracket notation when supplying a port.
|
||||
fn parse_external_advert_addr(raw: &str, bind_port: u16) -> Option<SocketAddr> {
|
||||
if let Ok(sa) = raw.parse::<SocketAddr>() {
|
||||
@@ -79,8 +79,8 @@ pub struct UdpConfig {
|
||||
/// Optional explicit public address to advertise when `public: true`
|
||||
/// is set. Takes precedence over both the bound address and any
|
||||
/// STUN-derived autodiscovery. Accepts either a bare IP
|
||||
/// (`"54.183.70.180"` — the configured `bind_addr` port is appended)
|
||||
/// or a full `host:port` (`"54.183.70.180:443"`). Useful when the
|
||||
/// (`"198.51.100.1"` — the configured `bind_addr` port is appended)
|
||||
/// or a full `host:port` (`"198.51.100.1:443"`). Useful when the
|
||||
/// public IP isn't on a local interface (e.g. AWS EIP / cloud 1:1
|
||||
/// NAT) and the operator wants to skip STUN autodiscovery for a
|
||||
/// deterministic value.
|
||||
@@ -414,7 +414,7 @@ pub struct TcpConfig {
|
||||
/// Optional explicit public address to advertise. Required when
|
||||
/// `bind_addr` is wildcard (e.g. `"0.0.0.0:443"`) and
|
||||
/// `advertise_on_nostr: true`, since TCP has no STUN equivalent
|
||||
/// for autodiscovery. Accepts either a bare IP (`"54.183.70.180"`
|
||||
/// for autodiscovery. Accepts either a bare IP (`"198.51.100.1"`
|
||||
/// — the configured `bind_addr` port is appended) or a full
|
||||
/// `host:port`. Common pattern on AWS EIP / cloud 1:1 NAT setups
|
||||
/// where the public IP isn't bindable on the host.
|
||||
@@ -875,14 +875,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_external_addr_accepts_bare_ipv4_with_appended_bind_port() {
|
||||
let sa = parse_external_advert_addr("54.183.70.180", 2121).unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:2121");
|
||||
let sa = parse_external_advert_addr("198.51.100.1", 2121).unwrap();
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:2121");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_external_addr_accepts_full_ipv4_socket_addr() {
|
||||
let sa = parse_external_advert_addr("54.183.70.180:443", 2121).unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:443");
|
||||
let sa = parse_external_advert_addr("198.51.100.1:443", 2121).unwrap();
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:443");
|
||||
// Explicit port wins over the bind port we passed in.
|
||||
}
|
||||
|
||||
@@ -907,23 +907,23 @@ mod tests {
|
||||
#[test]
|
||||
fn udp_external_advert_addr_combines_with_bind_port_default() {
|
||||
let cfg = UdpConfig {
|
||||
external_addr: Some("54.183.70.180".to_string()),
|
||||
external_addr: Some("198.51.100.1".to_string()),
|
||||
..UdpConfig::default()
|
||||
};
|
||||
// bind_addr unset, so default DEFAULT_UDP_BIND_ADDR (0.0.0.0:2121) applies.
|
||||
let sa = cfg.external_advert_addr().unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:2121");
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:2121");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udp_external_advert_addr_with_explicit_full_socket_addr_overrides_bind_port() {
|
||||
let cfg = UdpConfig {
|
||||
bind_addr: Some("0.0.0.0:2121".to_string()),
|
||||
external_addr: Some("54.183.70.180:9999".to_string()),
|
||||
external_addr: Some("198.51.100.1:9999".to_string()),
|
||||
..UdpConfig::default()
|
||||
};
|
||||
let sa = cfg.external_advert_addr().unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:9999");
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:9999");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -935,7 +935,7 @@ mod tests {
|
||||
#[test]
|
||||
fn tcp_external_advert_addr_requires_bind_port() {
|
||||
let cfg = TcpConfig {
|
||||
external_addr: Some("54.183.70.180".to_string()),
|
||||
external_addr: Some("198.51.100.1".to_string()),
|
||||
..TcpConfig::default()
|
||||
};
|
||||
// bind_addr unset → no port to combine with → None.
|
||||
@@ -943,22 +943,22 @@ mod tests {
|
||||
|
||||
let cfg = TcpConfig {
|
||||
bind_addr: Some("0.0.0.0:443".to_string()),
|
||||
external_addr: Some("54.183.70.180".to_string()),
|
||||
external_addr: Some("198.51.100.1".to_string()),
|
||||
..TcpConfig::default()
|
||||
};
|
||||
let sa = cfg.external_advert_addr().unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:443");
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_external_advert_addr_with_full_socket_addr_independent_of_bind() {
|
||||
let cfg = TcpConfig {
|
||||
bind_addr: Some("0.0.0.0:443".to_string()),
|
||||
external_addr: Some("54.183.70.180:8443".to_string()),
|
||||
external_addr: Some("198.51.100.1:8443".to_string()),
|
||||
..TcpConfig::default()
|
||||
};
|
||||
let sa = cfg.external_advert_addr().unwrap();
|
||||
assert_eq!(sa.to_string(), "54.183.70.180:8443");
|
||||
assert_eq!(sa.to_string(), "198.51.100.1:8443");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,6 +8,6 @@ FIPS_NSEC=e752b92aed3ac1595807f5d0eb5125589fbec0a2cfd3a2948d87ea076557deeb
|
||||
|
||||
# Peer configuration (leave empty for standalone operation)
|
||||
FIPS_PEER_NPUB=npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98
|
||||
FIPS_PEER_ADDR=217.77.8.91:2121
|
||||
FIPS_PEER_ADDR=test-us01.fips.network:2121
|
||||
FIPS_PEER_ALIAS=vps
|
||||
FIPS_PEER_TRANSPORT=udp
|
||||
|
||||
@@ -38,7 +38,7 @@ nodes:
|
||||
# External/public node (not a Docker container)
|
||||
pub:
|
||||
npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
||||
external_ip: "217.77.8.91"
|
||||
external_ip: "test-us01.fips.network"
|
||||
peers: [a, b, c]
|
||||
|
||||
# Spanning Tree Structure (rooted at node A):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# FIPS Tor test node A — socks5-outbound
|
||||
#
|
||||
# Connects outbound to test-us01 (217.77.8.91:443) via Tor SOCKS5 proxy.
|
||||
# Connects outbound to test-us01 (test-us01.fips.network:443) via Tor SOCKS5 proxy.
|
||||
# Identity generated per-run to avoid mesh clashes with parallel tests.
|
||||
|
||||
node:
|
||||
@@ -24,5 +24,5 @@ peers:
|
||||
alias: "test-us01"
|
||||
addresses:
|
||||
- transport: tor
|
||||
addr: "217.77.8.91:443"
|
||||
addr: "test-us01.fips.network:443"
|
||||
connect_policy: auto_connect
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# FIPS Tor test node B — socks5-outbound
|
||||
#
|
||||
# Connects outbound to test-us01 (217.77.8.91:443) via Tor SOCKS5 proxy.
|
||||
# Connects outbound to test-us01 (test-us01.fips.network:443) via Tor SOCKS5 proxy.
|
||||
# Identity generated per-run to avoid mesh clashes with parallel tests.
|
||||
|
||||
node:
|
||||
@@ -24,5 +24,5 @@ peers:
|
||||
alias: "test-us01"
|
||||
addresses:
|
||||
- transport: tor
|
||||
addr: "217.77.8.91:443"
|
||||
addr: "test-us01.fips.network:443"
|
||||
connect_policy: auto_connect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Tor transport integration test — socks5-outbound
|
||||
#
|
||||
# Topology:
|
||||
# [fips-a] --tor/socks5--> test-us01 (217.77.8.91:443) <--tor/socks5-- [fips-b]
|
||||
# [fips-a] --tor/socks5--> test-us01 (test-us01.fips.network:443) <--tor/socks5-- [fips-b]
|
||||
#
|
||||
# Both FIPS nodes connect outbound through a local Tor daemon's SOCKS5
|
||||
# proxy to test-us01's TCP listener. test-us01 routes between them.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# fips-a --tor/socks5--> test-us01 <--tor/socks5-- fips-b
|
||||
#
|
||||
# Both local FIPS nodes connect outbound through a local Tor daemon
|
||||
# to test-us01's TCP listener (217.77.8.91:443). Once both are peered
|
||||
# to test-us01's TCP listener (test-us01.fips.network:443). Once both are peered
|
||||
# with test-us01, traffic between fips-a and fips-b is routed through it.
|
||||
#
|
||||
# Each run generates ephemeral identities to avoid mesh clashes when
|
||||
|
||||
Reference in New Issue
Block a user