mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Optional peer discovery and NAT hole-punching path gated behind a new
`nostr-discovery` cargo feature. Nodes publish signed overlay endpoint
adverts to public Nostr relays, consume peer adverts to populate
fallback dial addresses, and use STUN-assisted UDP hole punching with
NIP-59 gift-wrap offer/answer signaling to establish direct UDP paths
between NATed peers. Once a punched socket is up, it is handed into
the existing FIPS UDP transport and the standard Noise/FMP session
stack takes over unchanged.
The cargo feature is in the default feature set
(`default = ["nostr-discovery"]`) so stock builds include it; a
build that explicitly disables default features (or selects a
feature set without `nostr-discovery`) does not link the nostr /
nostr-sdk crates and does not emit a no-op poll in the tick loop.
Runtime behavior is independently gated by
`node.discovery.nostr.enabled`, which defaults to false; if the
config enables Nostr on a non-feature build, startup logs a
warning and continues without it.
== Cargo feature and dependencies
- New cargo feature `nostr-discovery = ["dep:nostr", "dep:nostr-sdk"]`.
Not in the default feature set.
- New optional Linux-only dependencies: `nostr 0.44` (features: std,
nip59) and `nostr-sdk 0.44`. Gift-wrap unwrap is hand-rolled in
`src/discovery/nostr/signal.rs` rather than relying on the SDK's
rumor-author check, which FIPS sidesteps by trusting `seal.pubkey`
exclusively.
== Wire format
Overlay advert event: `kind 37195`, parameterized replaceable
(NIP-01 application-defined replaceable range 30000-39999), with
`d = "fips-overlay-v1"`. The digits visually spell FIPS (7=F, 1=I,
9=P, 5=S); a relay survey confirmed the kind is unused.
Advert content carries the version tag, endpoint list
(`udp|tcp|tor` + addr), optional signal-relay and stun-server
metadata, and `issuedAt` / `expiresAt` timestamps. Endpoint
`addr: "nat"` is the sentinel that triggers traversal on the peer
side. NIP-40 `expiration` tag bounds staleness on permanent
shutdown. Lifecycle relies on parameterized-replaceable
supersession; the daemon does not emit NIP-09 kind-5 deletes —
strict relays (Damus, Primal) race delete-against-replace and can
silently drop the replacement.
Gift-wrapped signal event: `kind 21059`. Punch packets carry magic
values `PUNCH_MAGIC` / `PUNCH_ACK_MAGIC`, a sequence number, and a
16-byte session hash.
== Discovery surface
- `src/discovery.rs` (always compiled)
- `EstablishedTraversal`: bound UDP socket + selected remote +
peer npub + optional transport name/config tuning overrides.
- `BootstrapHandoffResult`: returned on successful handoff —
allocated transport id, local/remote addrs, peer NodeAddr,
session id.
- `src/discovery/nostr/` (`#![cfg(feature = "nostr-discovery")]`)
- `types.rs`: wire and control types described above. `ADVERT_KIND`
constant. `BootstrapError` enumerates failure modes (disabled,
missing advert, missing NAT endpoint, no usable relays, invalid
advert, invalid npub, signal timeout, punch timeout, replay,
STUN failure, protocol, nostr, io, serde, event-parse).
- `runtime.rs`: `NostrDiscovery` coordinator. Owns the shared
nostr-sdk `Client`, subscribes to advert + signal event kinds,
maintains a bounded advert cache and a bounded seen-sessions
replay set, drains `BootstrapEvent::{Established, Failed}` for
the node to consume, exposes `update_local_advert`,
`request_connect`, `advert_endpoints_for_peer`,
`cached_open_discovery_candidates`, and `shutdown`.
- `signal.rs`: NIP-59 gift-wrap encode/decode. Outbound wraps are
built against per-attempt ephemeral keys; inbound events are
unwrapped against the node identity.
- `stun.rs`: RFC 5389/8489 Binding Request client with
XOR-MAPPED-ADDRESS parsing for both IPv4 and IPv6; used only to
observe the initiator's own reflexive address against its
locally configured STUN list (peer-advertised STUN is
informational, never an egress target).
- `traversal.rs`: per-attempt candidate-pair punch planner.
Allocates a fresh `0.0.0.0:0` UDP socket per attempt, enumerates
LAN-private and ULA interface addresses alongside the STUN
reflexive address, schedules probe/ack exchanges at the
configured interval for the configured duration, and picks the
first candidate pair that authenticates end-to-end.
Strategy ordering is Reflexive↔Reflexive first, then LAN, then
Mixed. The STUN-observed pair is the only candidate that's reliable
across arbitrary network topologies; trying it first prevents the
planner from latching onto a misleading host-candidate path before
the reflexive path gets a chance. There is no catch-all
Local↔Local strategy: a previous design that paired every local
host candidate from one side with every local host candidate from
the other could declare success on a one-way reachable asymmetric
L3 path (corporate VPN, Tailscale subnet route, overlapping private
address space), only for the FMP handshake to stall because the
return path didn't match. The legitimate `Lan` strategy still pairs
candidates that share a subnet.
== Configuration surface
`node.discovery.nostr.*` (`NostrDiscoveryConfig`), all `serde(default)`
with `deny_unknown_fields`:
- `enabled` (default false), `advertise` (default true)
- `advert_relays`, `dm_relays`, `stun_servers`: defaults are
`wss://relay.damus.io`, `wss://nos.lol`, `wss://offchain.pub`
for both relay lists, and Google / Cloudflare / Twilio for STUN.
Operators are expected to override for production. Other
verified-working public relays for reference:
`nostr.bitcoiner.social`, `nostr-pub.wellorder.net`,
`nostr.oxtr.dev`, `nostr.mom`.
- `app` (default `"fips-overlay-v1"`), `signal_ttl_secs` (120)
- `policy`: `NostrDiscoveryPolicy::{Disabled, ConfiguredOnly (default),
Open}` — controls whether advert-derived endpoints are consumed
only for peers carrying `via_nostr = true`, or also for
non-configured peers within a budget cap.
- `share_local_candidates` (default false) — when false, the offer's
`local_addresses` list is empty and peers see only the reflexive
address. Enable per-node only for genuinely same-LAN deployments;
off-by-default eliminates the misleading-path failure mode for
the common case where peers are not on the same broadcast domain.
- `open_discovery_max_pending` (64) — caps queued open-discovery
retries; bounded by available outbound slots.
- `max_concurrent_incoming_offers` (16) — semaphore against offer
spam; excess offers are debug-logged and dropped.
- `advert_cache_max_entries` (2048) and `seen_sessions_max_entries`
(2048) — bound memory under ambient relay volume; overflow
evictions are debug-logged.
- `attempt_timeout_secs` (10), `replay_window_secs` (300)
- `punch_start_delay_ms` (2000), `punch_interval_ms` (200),
`punch_duration_ms` (10000)
- `advert_ttl_secs` (3600), `advert_refresh_secs` (1800)
Per-peer and per-transport flags:
- `PeerConfig.via_nostr: bool` — when true (and Nostr is enabled),
advert-derived addresses are appended as fallback dial candidates
after static addresses for that peer.
- `PeerConfig.addresses` is now `serde(default)` and may be empty
when `via_nostr: true`; validation requires at least one of the
two to be present per peer, and the error message names the
peer's npub.
- `UdpConfig.advertise_on_nostr: Option<bool>` and
`UdpConfig.public: Option<bool>` — UDP transports can be
advertised either as direct `host:port` (public = true) or as the
`addr: "nat"` sentinel that triggers rendezvous on the peer side.
- `TcpConfig.advertise_on_nostr` and `TorConfig.advertise_on_nostr`
— TCP and Tor onion endpoints can be advertised as directly
reachable.
- A reserved peer address `transport: udp, addr: "nat"` parses without
special-casing in YAML and routes through the bootstrap runtime.
Cross-field validation (`Config::validate`, called from `Node::new`
and `Node::with_identity`):
- Any transport with `advertise_on_nostr = true` requires
`node.discovery.nostr.enabled = true`.
- Any peer with `via_nostr = true` requires
`node.discovery.nostr.enabled = true`.
- A non-public UDP advert (`advertise_on_nostr = true`,
`public = false` — i.e. `udp:nat`) additionally requires at least
one `dm_relay` and at least one `stun_server`.
Surfaced as `ConfigError::Validation`.
== Node integration
`src/node/lifecycle.rs` is the main integration point.
- At node start (after transports are up, before TUN), if Nostr is
enabled and the feature is compiled in, `NostrDiscovery::start` is
invoked, the initial local overlay advert is built from the live
transport set and published, and the runtime handle is stored.
- The rx tick loop calls `poll_nostr_discovery` (feature-gated both
at method definition and call site), which refreshes the local
advert, drains bootstrap events, adopts established traversals,
schedules retries for failed traversals, and — under `policy:
open` — enqueues outbound retries for non-configured peers
visible in the advert cache, bounded by
`open_discovery_max_pending` and the remaining outbound slots.
- Outbound peer dialing is refactored to `try_peer_addresses`, which
first exhausts the static address list in priority order and only
then appends advert-derived fallback addresses; both lists run
through the same `attempt_peer_address_list` code path. The
`udp:nat` sentinel address triggers `NostrDiscovery::request_connect`
for the peer instead of a direct dial and returns `Ok(())`.
- `build_overlay_advert` walks operational transports, consults
per-instance `UdpConfig` / `TcpConfig` / `TorConfig` (matching by
optional transport instance name), and emits an `OverlayAdvert`
including `signalRelays` and `stunServers` when any UDP endpoint
is advertised as NAT.
- `adopt_established_traversal` is the bootstrap handoff API:
allocates a new `TransportId`, constructs a `UdpTransport` with
the user-supplied (or default) `UdpConfig`, calls the new
`adopt_socket_async` to reuse the punched socket verbatim,
registers the transport in the normal transport map, records it
in `bootstrap_transports`, and calls `initiate_connection` so the
normal handshake path runs. On failure, the transport is stopped
and removed cleanly and the set membership is rolled back.
- On clean shutdown, `NostrDiscovery::shutdown` is awaited so
background tasks stop before transports are torn down. (The
advert is not explicitly retracted; NIP-40 expiration plus the
next refresh from any live publisher supersedes it.)
New `Node` fields:
- `nostr_discovery: Option<Arc<NostrDiscovery>>` (feature-gated).
- `bootstrap_transports: HashSet<TransportId>` — per-peer UDP
transports adopted from NAT traversal, cleaned up via
`cleanup_bootstrap_transport_if_unused` whenever the link,
connection, peer, or pending-connect referencing them is removed.
Retry and error surface:
- `RetryState.expires_at_ms: Option<u64>` — optional absolute expiry
for a retry entry. `pump_retries` drops expired entries with an
info log. Used for open-discovery retries, which expire at two
times the advert TTL.
- New `NodeError::BootstrapHandoff(String)` returned from
`adopt_established_traversal` when the underlying transport
adoption fails or local address discovery fails.
- New `ConfigError::Validation(String)`.
- A small refactor extracts `Node::now_ms()` and reuses it across
lifecycle, rx-loop tick, and timeout bookkeeping.
== UDP transport
`src/transport/udp/`:
- `UdpRawSocket::adopt(std::net::UdpSocket, recv_buf, send_buf)`:
adopts an externally bound socket, makes it non-blocking, applies
the configured buffer sizes (warning if the kernel clamps), and
reports the resulting local address. Preserves the NAT mapping —
no rebind.
- `UdpTransport::adopt_socket_async(std::net::UdpSocket)`: the
`start_async` analogue for an already-bound socket, wiring the
async socket and recv task exactly as the fresh-bind path would.
- `Drop` impl for `UdpTransport`: if a transport is dropped while
still holding a recv task or socket (for example on error
teardown), aborts the task, clears the socket, and emits a debug
log so the cleanup is visible in tracing rather than silent.
== Logging and observability
Default `EnvFilter` demotes third-party relay-pool DEBUG output to
TRACE-only: `nostr_relay_pool`, `nostr_sdk`, and `nostr` are pinned
at INFO when our level is anything below TRACE, and at TRACE when
our level is TRACE — so the raw frames are still reachable when
explicitly asked for. RUST_LOG continues to override completely.
Concise one-line DEBUG events are emitted at the meaningful points
in the discovery / hole-punch sequence:
- `advert: published` (event id, relay count, endpoints, ttl)
- `advert: peer cached` (notify-loop ingress for non-self)
- `advert: resolved` (cache hit / relay fetch outcome)
- `traversal: initiator starting`
- `traversal: initiator STUN observed` (reflexive, local count)
- `traversal: offer sent` (session id, relay count, event id)
- `traversal: answer received` (accepted, reflexive, local)
- `traversal: initiator punch succeeded` (remote addr)
- `traversal: offer received` (responder side)
- `traversal: responder STUN observed`
- `traversal: answer sent`
- `traversal: responder punch succeeded`
Npubs are shortened to `npub1<4>..<4>` and event/session ids to
their first 8 hex characters.
Other operator-facing logs:
- `UdpTransport` adoption and drop paths log at info / debug.
- `adopt_established_traversal` logs at debug on entry and info on
successful return, tagged with peer npub, session id, transport
id, and both socket endpoints, so the bootstrap handoff is
traceable end-to-end alongside the `UdpTransport::drop` log.
- `cleanup_bootstrap_transport_if_unused` logs at debug when the
reference-count check drops an adopted transport.
- `connect_peer` tags its entry `debug!` with `peer_npub` so
downstream STUN, punch, and handshake logs for the same peer
correlate for operators.
- Advert-cache and seen-sessions overflow evictions log at debug so
mis-sized caps are visible under ambient relay volume.
- Gift-wrap unwrap failures on `SIGNAL_KIND` events log at trace
(hot path: fires for every unrelated signal event on the same
relay).
- Traversal-offer handler failures log at debug. Expected conditions
such as punch timeout on symmetric NAT are covered there; real
problems are reported upstream via `BootstrapEvent::Failed`.
- Inbound-offer rate-limit messages name the governing config field
(`max_concurrent_incoming_offers`) and state that the offer was
rate-limited rather than failing.
== Tests
- 18 new unit tests in `src/discovery/nostr/tests.rs` covering advert
encoding, signal envelope round-trip, STUN parsing, punch-packet
codec, and replay-window enforcement. Run under the
`nostr-discovery` feature.
- Config-validation tests in `src/config/mod.rs` covering the three
cross-field invariants and YAML parsing of the full
`node.discovery.nostr` block plus `peers[].via_nostr`, empty
`addresses` with `via_nostr: true`, and a `udp: nat` address.
- `src/node/tests/bootstrap.rs` integration tests that drive a
synthetic traversal (bound UDP socket pair + synthetic peer
identity) through `adopt_established_traversal` and assert the
Noise handshake completes over the adopted socket.
- Punch-planner tests assert reflexive-before-LAN ordering and that
same-LAN scenarios still include the LAN target in the plan.
- `testing/nat/` Docker NAT lab harness:
- Local `strfry` relay, local STUN responder, and one or two
router containers performing `iptables` NAT.
- Node LAN interfaces are provisioned with explicit `veth` pairs
injected into the node and router namespaces so every packet
traverses the router namespace (plain Docker bridges are not
used for the LAN).
- `cone` scenario: both peers behind full-cone-emulation NAT
(SNAT with source-port preservation, inbound DNAT back to the
single LAN host regardless of remote source); asserts UDP
traversal succeeds and link remote addresses are on the router
WAN subnet.
- `symmetric` scenario: `MASQUERADE --random-fully`; asserts UDP
traversal fails and TCP fallback converges over router-
published WAN addresses.
- `lan` scenario: both peers share a LAN subnet; asserts LAN
addresses are preferred over reflexive ones.
- Cleanup tears down all profile-gated services
(`--profile cone --profile symmetric --profile lan`) so no
orphan containers survive a run.
- `testing/scripts/build.sh` builds the Docker test image with
`--features "tui nostr-discovery"` by default so NAT-harness
binaries include bootstrap support.
== CI
- Linux release build and nextest unit-test job both use
`--features "gateway nostr-discovery"` so the feature-gated code
and its unit tests compile and run in CI.
- Three new integration matrix entries (`nat-cone`, `nat-symmetric`,
`nat-lan`) invoke `testing/nat/scripts/nat-test.sh`, collect
`docker compose logs` on failure, and always stop containers.
== Packaging and operations
- `packaging/common/fips.yaml` ships a fully commented
`node.discovery.nostr.*` block, plus documented
`advertise_on_nostr` / `public` examples under the UDP transport,
an `advertise_on_nostr` example under TCP, and a `via_nostr: true`
example under the static peer section with both a direct
`host:port` UDP address and a `udp: nat` fallback.
- `.github/workflows/package-openwrt.yml`: NIP-94 release event
publishes target the new default relay set.
== Documentation
- `README.md`: overlay discovery + NAT traversal moved from
"Near-term priorities" into "What works today".
- `docs/design/fips-intro.md`: rewrites the paragraphs that
previously described Nostr discovery and NAT traversal as future
work; describes the shipped mechanism and the feature gate.
- `docs/design/fips-transport-layer.md`: drops the "(future
direction)" qualifier from the Nostr Relay Discovery section,
expands with the `udp:nat` advertisement and bootstrap handoff
description, and updates the Current State callout.
- `docs/design/fips-mesh-layer.md`: notes that mid-session NAT
rebinding (roaming) and initial NAT traversal (Nostr path) are
distinct mechanisms.
- `docs/design/fips-configuration.md`: documents the full
`node.discovery.nostr.*` surface, including the three resource
caps and `share_local_candidates`.
- `docs/design/fips-nostr-discovery.md`: design and configuration
reference for the shipped mechanism, including the empty-
`addresses`-with-`via_nostr` shorthand.
- `docs/proposals/nostr-udp-hole-punch-protocol.md`: adds an
Implemented status callout, clarifies that the punch socket is
per-peer and per-attempt rather than shared with the application
listener, aligns field names with the shipped JSON
(`sessionId`, `issuedAt` / `expiresAt`, `reflexiveAddress`,
`localAddresses`, `stunServer`), sets the `d`-tag to
`fips-overlay-v1`, names the kind as 37195, and notes that
advertised STUN entries are informational.
- `docs/proposals/README.md`: adds a Status column and marks the
hole-punching proposal Implemented.
- `CHANGELOG.md`: Unreleased > Added entry covering the discovery
path, STUN/punch path, configuration surface, and Docker NAT lab.
Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
423 lines
14 KiB
Markdown
423 lines
14 KiB
Markdown
# FIPS: Free Internetworking Peering System
|
|
|
|

|
|
[](LICENSE)
|
|
[](https://www.rust-lang.org/)
|
|
[](#status--roadmap)
|
|
|
|
A distributed, decentralized network routing protocol for mesh nodes
|
|
connecting over arbitrary transports.
|
|
|
|
> FIPS is under active development. The protocol and APIs are not yet stable.
|
|
> See [Status & Roadmap](#status--roadmap) below.
|
|
|
|
## Overview
|
|
|
|
FIPS is a self-organizing mesh network that operates natively over a variety
|
|
of physical and logical media — local area networks, Bluetooth, serial links,
|
|
radio, or the existing internet as an overlay. Nodes generate their own
|
|
identities, discover each other, and route traffic without any central
|
|
authority or global topology knowledge.
|
|
|
|
FIPS uses Nostr keypairs (secp256k1/schnorr) as native node identities,
|
|
allowing users to generate their own persistent or ephemeral node addresses.
|
|
Nodes address each other by npub, and the same cryptographic identity serves
|
|
as both the routing address and the basis for end-to-end encrypted sessions
|
|
across the mesh.
|
|
|
|
FIPS allows existing TCP/IP based network software to use the FIPS mesh
|
|
network by generating a local IP address from the node npub and tunnelling
|
|
IP packets to other endpoints transparently knowing only their npub. Native
|
|
FIPS-aware applications do not need this IP tunneling or emulation capability.
|
|
|
|
All traffic over the FIPS mesh is encrypted and authenticated both
|
|
hop-to-hop between peers and independently end-to-end between FIPS
|
|
endpoints.
|
|
|
|
## Features
|
|
|
|
- **Self-organizing mesh routing** — spanning tree coordinates with bloom
|
|
filter guided discovery, no global routing tables
|
|
- **Multi-transport** — UDP, TCP, Ethernet, Tor, and Bluetooth (BLE L2CAP)
|
|
today; designed for serial and radio
|
|
- **Noise encryption** — hop-by-hop link encryption (IK) plus independent
|
|
end-to-end session encryption (XK), with periodic rekey for forward secrecy
|
|
- **Nostr-native identity** — secp256k1 keypairs as node addresses, no
|
|
registration or central authority
|
|
- **IPv6 adaptation** — TUN interface maps npubs to fd00::/8 addresses
|
|
for unmodified IP applications; built-in `.fips` DNS resolver with
|
|
optional static hostname mapping (`/etc/fips/hosts`)
|
|
- **Outbound LAN gateway** — optional `fips-gateway` daemon lets
|
|
unmodified LAN hosts reach `.fips` destinations via a
|
|
DNS-allocated virtual IP pool and kernel nftables NAT
|
|
- **Metrics Measurement Protocol** — per-link RTT, loss, jitter, and goodput
|
|
measurement with mesh size estimation
|
|
- **ECN congestion signaling** — hop-by-hop CE flag relay with RFC 3168 IPv6
|
|
marking, transport kernel drop detection
|
|
- **Operator visibility** — `fipsctl` CLI and `fipstop` TUI dashboard for
|
|
runtime inspection and runtime peer management
|
|
- **Zero configuration** — sensible defaults; a node can start with no config
|
|
file, though peer addresses are needed to join a network
|
|
|
|
## Building
|
|
|
|
```bash
|
|
git clone https://github.com/jmcorgan/fips.git
|
|
cd fips
|
|
cargo build --release
|
|
```
|
|
|
|
Requires Rust 1.85+ (edition 2024). Linux, macOS, and Windows are
|
|
supported (see transport matrix below).
|
|
|
|
### Transport support by platform
|
|
|
|
| Transport | Linux | macOS | Windows | OpenWrt |
|
|
|-----------|:-----:|:-----:|:-------:|:-------:|
|
|
| UDP | ✅ | ✅ | ✅ | ✅ |
|
|
| TCP | ✅ | ✅ | ✅ | ✅ |
|
|
| Ethernet | ✅ | ✅ | ❌ | ✅ |
|
|
| Tor | ✅ | ✅ | ✅ | ✅ |
|
|
| BLE | ✅ | ❌ | ❌ | ❌ |
|
|
|
|
On **Linux**, the BLE transport requires BlueZ and libdbus. On
|
|
Debian/Ubuntu: `sudo apt install bluez libdbus-1-dev`. Then build with
|
|
BLE enabled: `cargo build --release --features ble`.
|
|
|
|
On **OpenWrt**, BLE is disabled because libdbus is not available on
|
|
the target. All other transports work and ship in the default ipk.
|
|
|
|
## Installation
|
|
|
|
After building, choose one of the following methods to install.
|
|
|
|
### Debian / Ubuntu (.deb)
|
|
|
|
Requires [cargo-deb](https://crates.io/crates/cargo-deb):
|
|
|
|
```bash
|
|
cargo install cargo-deb
|
|
cargo deb
|
|
sudo dpkg -i target/debian/fips_*.deb
|
|
```
|
|
|
|
This installs the daemon, CLI tools, systemd units, and a default
|
|
configuration. Edit `/etc/fips/fips.yaml` before starting:
|
|
|
|
```bash
|
|
sudo nano /etc/fips/fips.yaml
|
|
sudo systemctl start fips
|
|
```
|
|
|
|
The service is enabled at boot automatically. To use `fipsctl` and
|
|
`fipstop` without sudo, add your user to the `fips` group:
|
|
|
|
```bash
|
|
sudo usermod -aG fips $USER # log out and back in to take effect
|
|
```
|
|
|
|
Remove with `sudo dpkg -r fips` (preserves config) or
|
|
`sudo dpkg -P fips` (removes everything including identity keys).
|
|
|
|
### Generic Linux (systemd tarball)
|
|
|
|
```bash
|
|
./packaging/systemd/build-tarball.sh
|
|
tar xzf deploy/fips-*-linux-*.tar.gz
|
|
cd fips-*-linux-*/
|
|
sudo ./install.sh
|
|
```
|
|
|
|
See [packaging/systemd/README.install.md](packaging/systemd/README.install.md)
|
|
for the full installation and configuration guide.
|
|
|
|
### macOS (.pkg)
|
|
|
|
```bash
|
|
./packaging/macos/build-pkg.sh
|
|
sudo installer -pkg deploy/fips-*-macos-*.pkg -target /
|
|
```
|
|
|
|
This installs binaries to `/usr/local/bin/`, config to
|
|
`/usr/local/etc/fips/`, sets up `.fips` DNS resolution via
|
|
`/etc/resolver/fips`, and registers a launchd daemon. Edit
|
|
`/usr/local/etc/fips/fips.yaml` before starting:
|
|
|
|
```bash
|
|
sudo nano /usr/local/etc/fips/fips.yaml
|
|
sudo launchctl load -w /Library/LaunchDaemons/com.fips.daemon.plist
|
|
```
|
|
|
|
Remove with `sudo packaging/macos/uninstall.sh` (preserves config).
|
|
|
|
To restart the node after making configuration changes:
|
|
|
|
```bash
|
|
sudo launchctl unload -w /Library/LaunchDaemons/com.fips.daemon.plist
|
|
sudo launchctl load -w /Library/LaunchDaemons/com.fips.daemon.plist
|
|
```
|
|
|
|
Check logs for troubleshooting:
|
|
|
|
```bash
|
|
sudo tail -f /usr/local/var/log/fips/fips.log
|
|
```
|
|
|
|
> **Note:** On macOS, the TUN device is named `utun<N>` (kernel-assigned)
|
|
> rather than `fips0`.
|
|
|
|
### Windows
|
|
|
|
Build without BLE (requires Linux-only libdbus):
|
|
|
|
```powershell
|
|
cargo build --release --no-default-features --features tui
|
|
```
|
|
|
|
The [wintun](https://www.wintun.net/) driver is required for TUN support.
|
|
Download `wintun.dll` and place it in the same directory as `fips.exe`.
|
|
Running the daemon requires Administrator privileges for TUN creation.
|
|
|
|
**Foreground mode:**
|
|
|
|
```powershell
|
|
.\fips.exe -c fips.yaml
|
|
```
|
|
|
|
**Windows Service:**
|
|
|
|
```powershell
|
|
# Install (requires Administrator)
|
|
.\fips.exe --install-service
|
|
|
|
# Manage via standard service tools
|
|
sc start fips
|
|
sc stop fips
|
|
|
|
# Uninstall
|
|
.\fips.exe --uninstall-service
|
|
```
|
|
|
|
Place `fips.yaml` in the current directory or `%APPDATA%\fips\`, or set
|
|
the `FIPS_CONFIG` environment variable.
|
|
|
|
The control socket uses TCP on `localhost:21210` instead of a Unix domain
|
|
socket. `fipsctl` and `fipstop` connect to this port automatically.
|
|
|
|
## Configuration
|
|
|
|
The default configuration file is installed at `/etc/fips/fips.yaml`:
|
|
|
|
```yaml
|
|
# FIPS Node Configuration
|
|
|
|
node:
|
|
identity:
|
|
# 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
|
|
#
|
|
# Or set an explicit key (overrides persistent):
|
|
# nsec: "nsec1..."
|
|
|
|
tun:
|
|
enabled: true
|
|
name: fips0
|
|
mtu: 1280
|
|
|
|
dns:
|
|
enabled: true
|
|
bind_addr: "127.0.0.1"
|
|
port: 5354
|
|
|
|
transports:
|
|
udp:
|
|
bind_addr: "0.0.0.0:2121"
|
|
|
|
tcp:
|
|
# Accepts inbound connections. No static outbound peers.
|
|
bind_addr: "0.0.0.0:8443"
|
|
|
|
# Ethernet transport — uncomment and set your interface name.
|
|
# ethernet:
|
|
# interface: "eth0"
|
|
# discovery: true
|
|
# announce: true
|
|
# auto_connect: true
|
|
# accept_connections: true
|
|
|
|
peers:
|
|
# Static peers for bootstrapping (UDP or TCP):
|
|
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
|
|
alias: "fips-test-node"
|
|
addresses:
|
|
- transport: udp
|
|
addr: "217.77.8.91:2121"
|
|
connect_policy: auto_connect
|
|
```
|
|
|
|
See [docs/design/fips-configuration.md](docs/design/fips-configuration.md)
|
|
for the full reference.
|
|
|
|
## Usage
|
|
|
|
### DNS Resolution
|
|
|
|
FIPS includes a DNS resolver (enabled by default, port 5354) that maps
|
|
`.fips` names to fd00::/8 IPv6 addresses.
|
|
|
|
**Linux**: The `.deb` package auto-detects and configures whichever
|
|
resolver is present (systemd dns-delegate, systemd-resolved, dnsmasq,
|
|
or NetworkManager with dnsmasq); no manual setup is needed. For
|
|
manual or tarball installs, point your resolver at `127.0.0.1:5354`
|
|
for the `fips` domain — e.g., with systemd-resolved:
|
|
|
|
```bash
|
|
sudo resolvectl dns fips0 127.0.0.1:5354
|
|
sudo resolvectl domain fips0 ~fips
|
|
```
|
|
|
|
**macOS**: DNS is configured automatically by the `.pkg` installer via
|
|
`/etc/resolver/fips`. No manual setup is needed.
|
|
|
|
Then reach any FIPS node by npub with standard IPv6 tools:
|
|
|
|
```bash
|
|
ping6 npub1bbb....fips
|
|
ssh -6 npub1bbb....fips
|
|
```
|
|
|
|
> **macOS note:** Use `ping6` instead of `ping`. macOS ships separate
|
|
> `ping` (IPv4-only) and `ping6` (IPv6) binaries; `ping` will not
|
|
> resolve AAAA records. Similarly, use `curl -6`, `ssh -6`, etc. when
|
|
> connecting by `.fips` hostname.
|
|
|
|
### Monitoring
|
|
|
|
Use `fipsctl` to query a running node:
|
|
|
|
```bash
|
|
fipsctl show status # Node status overview
|
|
fipsctl show peers # Authenticated peers and security state
|
|
fipsctl show links # Active links
|
|
fipsctl show tree # Spanning tree state
|
|
fipsctl show sessions # End-to-end sessions and rekey health
|
|
fipsctl show bloom # Bloom filter state
|
|
fipsctl show mmp # MMP metrics summary
|
|
fipsctl show cache # Coordinate cache entries and routes
|
|
fipsctl show connections # Pending handshake connections
|
|
fipsctl show transports # Transport instances
|
|
fipsctl show routing # Routing, discovery, and retry state
|
|
fipsctl show identity-cache # Known node identities (npubs)
|
|
```
|
|
|
|
`fipstop` provides an interactive TUI dashboard with live-updating
|
|
views of node status, peers, links, sessions, tree state, transports,
|
|
and routing:
|
|
|
|
```bash
|
|
fipstop # connect to local daemon
|
|
fipstop -r 1 # 1-second refresh interval
|
|
```
|
|
|
|
### Service Management
|
|
|
|
```bash
|
|
sudo systemctl start fips
|
|
sudo systemctl stop fips
|
|
sudo systemctl restart fips
|
|
sudo journalctl -u fips -f
|
|
```
|
|
|
|
### Testing
|
|
|
|
See [testing/](testing/) for Docker-based integration test harnesses
|
|
including static topology tests and stochastic chaos simulation.
|
|
|
|
## Examples
|
|
|
|
- [examples/sidecar-nostr-relay/](examples/sidecar-nostr-relay/) —
|
|
Run a [strfry](https://github.com/hoytech/strfry) Nostr relay
|
|
reachable exclusively over the FIPS mesh. The relay container shares
|
|
the FIPS sidecar's network namespace and is isolated from the host
|
|
network.
|
|
- [examples/k8s-sidecar/](examples/k8s-sidecar/) — Run FIPS as a
|
|
Kubernetes Pod sidecar. The sidecar creates `fips0` in the Pod's
|
|
shared network namespace so every other container in the Pod gets
|
|
mesh access without modification.
|
|
- [examples/wireguard-sidecar-macos/](examples/wireguard-sidecar-macos/) —
|
|
Reach the FIPS mesh from a macOS host through a local Docker
|
|
container over a WireGuard tunnel. Only traffic destined for
|
|
`fd00::/8` transits the sidecar; regular internet traffic continues
|
|
to use the host network.
|
|
|
|
## Documentation
|
|
|
|
Protocol design documentation is in [docs/design/](docs/design/), organized as
|
|
a layered protocol specification. Start with
|
|
[fips-intro.md](docs/design/fips-intro.md) for the full protocol overview.
|
|
|
|
If you want to contribute, start with:
|
|
|
|
- [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
- [docs/design/README.md](docs/design/README.md)
|
|
- [testing/README.md](testing/README.md)
|
|
|
|
## Project Structure
|
|
|
|
```text
|
|
src/ Rust source (library + fips/fipsctl/fipstop/fips-gateway binaries)
|
|
packaging/ Debian, macOS .pkg, Windows ZIP, OpenWrt ipk, AUR, systemd tarball
|
|
examples/ Deployment examples (Nostr relay, K8s sidecar, macOS WireGuard)
|
|
docs/design/ Protocol design specifications
|
|
testing/ Docker-based integration test harnesses
|
|
```
|
|
|
|
## Status & Roadmap
|
|
|
|
FIPS is at **v0.2.0**. The core protocol works end-to-end over UDP, TCP,
|
|
Ethernet, Tor, and Bluetooth (BLE) with a small live mesh of deployed nodes.
|
|
|
|
### What works today
|
|
|
|
- Spanning tree construction with greedy coordinate routing
|
|
- Bloom filter guided discovery (no flooding, single-path with retry)
|
|
- Noise IK (link layer) and Noise XK (session layer) encryption
|
|
- Periodic Noise rekey with hitless cutover for forward secrecy (FMP + FSP)
|
|
- Persistent node identity with key file management
|
|
- IPv6 TUN adapter with built-in `.fips` DNS resolver and multi-backend
|
|
auto-configuration (systemd dns-delegate, systemd-resolved, dnsmasq,
|
|
NetworkManager)
|
|
- Static hostname mapping (`/etc/fips/hosts`) with auto-reload
|
|
- Per-link metrics (RTT, loss, jitter, goodput) and mesh size estimation
|
|
- ECN congestion signaling (hop-by-hop CE relay, IPv6 CE marking, kernel drop detection)
|
|
- UDP, TCP, Ethernet, Tor, and BLE transports (BLE via L2CAP CoC with per-link MTU negotiation)
|
|
- Outbound LAN gateway for unmodified hosts via DNS-allocated virtual IPs and nftables NAT
|
|
- Runtime inspection and peer management via `fipsctl` and `fipstop`
|
|
- Reproducible builds with toolchain pinning and SOURCE_DATE_EPOCH
|
|
- Linux (Debian, systemd tarball, OpenWrt, AUR), macOS (`.pkg`), and Windows (ZIP, service) packaging
|
|
- Docker-based integration and chaos testing
|
|
- Nostr-mediated overlay endpoint discovery and UDP hole punching for
|
|
NAT traversal — peers publish endpoint adverts on public Nostr
|
|
relays, exchange candidates via NIP-59 gift-wrapped offers/answers,
|
|
and establish direct paths through NATs using STUN-assisted
|
|
punching (behind the `nostr-discovery` cargo feature)
|
|
|
|
### Near-term priorities
|
|
|
|
- Native API for FIPS-aware applications (npub:port addressing)
|
|
- Security audit of cryptographic protocols
|
|
|
|
### Longer-term
|
|
|
|
- Mobile platform support
|
|
- Bandwidth-aware routing and QoS
|
|
- Protocol stability and versioned wire format
|
|
- Published crate
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|