Merge branch 'master' into next

This commit is contained in:
Johnathan Corgan
2026-05-10 22:04:29 +00:00
25 changed files with 406 additions and 130 deletions
+24
View File
@@ -480,9 +480,33 @@ with v0.2.x peers.
bind by setting `dns.listen: "[::]:53"` explicitly. The new
default binds IPv6 loopback only — forwarders that reach the
gateway over IPv4 loopback need an explicit IPv4 listen address.
- Generic systemd install tarball brought to feature parity with
the `.deb` and AUR packages. The tarball now ships the
`fips-gateway` binary with its (operator-opt-in)
`fips-gateway.service`, a `fips-firewall.service` unit with the
`/etc/fips/fips.nft` mesh-interface nftables baseline (also
opt-in), an `/etc/fips/fips.d/` operator drop-in directory for
per-service nft rules, and the multi-backend `fips-dns-setup` /
`fips-dns-teardown` helpers. `install.sh` and `uninstall.sh`
handle the new units and conffile (preserve-on-upgrade for
`fips.nft`, like `fips.yaml`). `README.install.md` documents
the gateway, firewall, and DNS-routing services. Closes the
longest-standing parity gap for non-Debian / non-Arch systemd
Linux distros (Fedora, RHEL/CentOS, openSUSE, etc.) installing
from the release-distribution tarball.
### Fixed
- Generic systemd install tarball: `install.sh` now correctly
resolves the `fips-dns-setup` and `fips-dns-teardown` helpers
from the tarball staging directory. Previously the script
referenced them at `${SCRIPT_DIR}/../common/`, a path that
exists only in the source-repo layout, not in the extracted
tarball. Bug latent since the multi-backend DNS helpers
landed in `7260ad2`; only manifested when operators ran
`install.sh` from an extracted tarball rather than from a
source checkout.
- Adopted NAT-traversed UDP transports inherit the primary listener's
MTU and buffer config. `Node::adopt_established_traversal`
constructed the adopted UDP transport with `UdpConfig::default()`
+2 -1
View File
@@ -17,7 +17,7 @@ Before changing code, read the protocol docs in this order:
## Prerequisites
- Rust 1.94.0 and Linux with TUN support
- Rust 1.94.1 and Linux with TUN support
- Use the pinned toolchain from [rust-toolchain.toml](rust-toolchain.toml) for deterministic builds
- For the default BLE-enabled build on Debian/Ubuntu:
`sudo apt install bluez libdbus-1-dev pkg-config`
@@ -74,6 +74,7 @@ See [testing/README.md](testing/README.md) for the available integration and cha
- Keep commits focused — one logical change per commit.
- Add tests for new functionality.
- Reference relevant design docs if the change touches protocol behavior.
- Pull requests are merged via squash-merge.
- Update docs in the same change when you modify:
- protocol or routing behavior
- wire formats
+12 -4
View File
@@ -66,9 +66,17 @@ same way it would on a local network.
Protocol.
- **ECN congestion signaling.** Hop-by-hop CE-flag relay with RFC
3168 IPv6 marking and transport kernel-drop detection.
- **Operator visibility.** `fipsctl` CLI for control and inspection,
`fipstop` TUI for live status, and a JSON-line control socket on
each binary for direct programmatic access.
- **Mesh-interface security baseline.** Optional default-deny
nftables policy for `fips0` shipped as a packaged conffile
(`/etc/fips/fips.nft`) with an operator drop-in directory
(`/etc/fips/fips.d/`) and a disabled-by-default
`fips-firewall.service`. The baseline polices only the mesh
interface, leaving Docker, Tor, and the host firewall untouched.
- **Operator visibility.** `fipsctl` CLI for control and inspection
with time-series stats history queryable for any metric,
`fipstop` TUI for live status with inline sparkline dashboards,
and a JSON-line control socket on each binary for direct
programmatic access.
- **Reproducible builds** with toolchain pinning and
`SOURCE_DATE_EPOCH`.
@@ -103,7 +111,7 @@ tutorial progression starting at
cargo build --release
```
Requires Rust 1.85+ (edition 2024). Linux, macOS, and Windows are
Requires Rust 1.94.1+ (edition 2024). Linux, macOS, and Windows are
supported; transport availability varies by platform.
| Transport | Linux | macOS | Windows | OpenWrt |
+3 -2
View File
@@ -15,8 +15,9 @@ every supported platform and ships in every stock packaging artifact
`.zip`). It is runtime-opt-in: the YAML configuration defaults to
disabled (`node.discovery.nostr.enabled: false`), so the discovery
runtime stays dormant — and opens no relay connections — until an
operator flips the flag and supplies a relay list. When disabled, nodes
behave exactly as before: only the static `peers[]` addresses are used.
operator flips the flag. Default relay and STUN-server lists ship in
the config; both are optional overrides. When disabled, nodes behave
exactly as before: only the static `peers[]` addresses are used.
## Role
+5 -4
View File
@@ -29,9 +29,10 @@ running nodes can mesh.
- A Linux, macOS, or Windows host. Linux is the most exercised
platform; macOS and Windows installers are available.
- The pre-built installer for your platform (see the
[Releases page](../README.md#installation)), **or** a source
checkout if you want to build the installer yourself.
- The pre-built installer for your platform (see the project
README's [Quick start](../README.md#quick-start) section for
download links), **or** a source checkout if you want to build
the installer yourself.
- For the source-build path only: a working Rust toolchain (the
version pinned in `rust-toolchain.toml` is auto-installed by
rustup), and the platform-specific build dependencies listed in
@@ -63,7 +64,7 @@ per-platform installer:
- Windows — `.zip` with service-install scripts
- Generic systemd Linux — `.tar.gz` with an `install.sh` script
See the [project README's Installation section](../README.md#installation)
See the [project README's Quick start section](../README.md#quick-start)
for download links and per-platform invocations.
### From source
+24 -10
View File
@@ -4,7 +4,8 @@ Nostr-mediated discovery lets FIPS nodes find each other (and punch
through UDP NAT) using public Nostr relays as the signaling channel.
The feature ships in every stock packaging artifact but is **off by
default** — it activates when an operator sets
`node.discovery.nostr.enabled: true` and provides a relay list. See
`node.discovery.nostr.enabled: true`. Default relay and STUN-server
lists ship in the config; both are optional overrides. See
[../design/fips-nostr-discovery.md](../design/fips-nostr-discovery.md)
for the design and rationale; see
[../reference/configuration.md](../reference/configuration.md) for the
@@ -126,15 +127,20 @@ transports:
udp:
bind_addr: "0.0.0.0:2121"
advertise_on_nostr: true
external_addr: "203.0.113.45:2121" # ← explicit
public: true # ← required, master switch
external_addr: "203.0.113.45:2121" # ← explicit address
```
`external_addr` accepts a bare IP (combined with the bind port) or a
full `host:port`. Setting both `public: true` and `external_addr`
together is allowed — the explicit override wins, with STUN as a
logging cross-check. If UDP is bound directly to a public IP rather
than to a wildcard, neither flag is needed; the daemon advertises
the bound endpoint.
full `host:port`. `public: true` is the master switch that gates UDP
advertisement; inside that branch, the daemon picks the advertised
address in precedence order: explicit `external_addr` (no STUN
observation), a non-wildcard `bind_addr`, or STUN auto-discovery.
Setting `external_addr` alongside `public: true` skips STUN entirely
— there is no logging cross-check. If UDP is bound directly to a
public IP rather than to a wildcard, neither `external_addr` nor STUN
is needed — but `advertise_on_nostr: true` and `public: true` are
still both required for the daemon to publish the endpoint.
What this achieves: the node publishes a single
`udp:<public-ip>:2121` endpoint to the three default advert relays
@@ -202,9 +208,17 @@ proceeds normally.
> a `udp:nat` advert without signaling relays or STUN servers is
> unreachable by construction.
Works best with full-cone NAT on at least one side. Symmetric NAT on
both sides is not reliably traversable with this protocol and will
time out after `punch_duration_ms`.
Hole-punching is best-effort. It works reliably when both sides are
full-cone or port-restricted NATs. Symmetric NAT on either side
typically defeats the punch — the public port a peer sees varies per
remote endpoint, so the address learned via STUN does not match the
mapping the peer actually needs. The punch attempt times out after
`punch_duration_ms`. `udp:nat` is the only NAT-traversal mechanism
in FIPS; when it can't succeed, there's no in-protocol substitute.
Being reachable then becomes a deployment-prerequisite question
rather than a transport question — a publicly reachable port (UDP
or TCP — both require the same kind of network resource) published
as a direct advert per Sub-scenario 2a or 2b.
### Sub-scenario 2b: TCP
+1 -1
View File
@@ -59,7 +59,7 @@ peers: # Static peer list
The control socket provides access to node state and runtime management
via the `fipsctl` command-line tool. In addition to read-only status
queries, `fipsctl connect` and `fipsctl disconnect` enable runtime peer
management. See the project [README](../../README.md#inspect) for the
management. See the [`fipsctl` reference](cli-fipsctl.md) for the
command list.
On Linux, the control socket is a Unix domain socket with filesystem
+1 -1
View File
@@ -25,7 +25,7 @@ cover every option.
| 1 | [join-the-test-mesh.md](join-the-test-mesh.md) | Add one public test peer to your config, watch the link come up, ping that peer and a second mesh node it routes you to. The starting point for everything else. |
| 2 | [persistent-identity.md](persistent-identity.md) | Pin your daemon to a stable Nostr keypair so your address stops changing on every restart. Other operators can now add you to their `peers:` lists; the services you host get a fixed name. |
| 3 | [resolve-peers-via-nostr.md](resolve-peers-via-nostr.md) | Stop hard-coding peer addresses. Drop the address line from your peer entry and let the daemon look up the current endpoint from public Nostr relays at dial time. |
| 4 | [advertise-your-node.md](advertise-your-node.md) | Publish your own UDP endpoint to Nostr so any operator who knows your npub can reach you, with a short final section on `udp:nat` traversal for symmetric-NAT networks. |
| 4 | [advertise-your-node.md](advertise-your-node.md) | Publish your own UDP endpoint to Nostr so any operator who knows your npub can reach you, with a short final section on `udp:nat` best-effort hole-punching for nodes without a directly reachable UDP endpoint. |
| 5 | [open-discovery.md](open-discovery.md) | Switch to `policy: open` and let your peer list populate itself from the ambient `fips-overlay-v1` namespace. Hands-off mesh participation. |
| 6 | [reach-mesh-services.md](reach-mesh-services.md) | Drive ordinary IPv6 tools — `ping6`, `nc`, `traceroute6`, `curl`, `ssh` — at mesh nodes by `<npub>.fips`. Get a feel for the daemon's IPv6 adapter, which makes unmodified IPv6 software work over the mesh. |
| 7 | [host-a-service.md](host-a-service.md) | Bring up an HTTP server bound to `fips0` so mesh nodes can reach it, with a deliberate exposure decision (mesh-only vs every interface), and the mesh firewall as a default-deny baseline. The peer ACL (a separate, transport-layer control over which npubs may peer with your node) is briefly mentioned alongside. |
+63 -41
View File
@@ -8,8 +8,9 @@ your own endpoint(s), so any other operator who knows your
npub can dial you the same way you dialed `test-us01`.
The whole exercise should take about ten minutes if you have
a public IP or full-cone home NAT. A short final section
covers the alternative path for symmetric-NAT networks.
a public IP or a UDP listener that's reachable from outside.
A short final section covers `udp:nat` best-effort hole-punching
for the cases where direct UDP advertising isn't an option.
## What you'll build
@@ -74,9 +75,13 @@ port to put in the advert:
> - `public: true` — daemon does a one-shot STUN observation
> against the configured STUN servers and uses the reflexive
> IPv4 it learns. Right when your public IP is dynamic or
> you'd rather not pin it in config. Works for nodes with a
> directly-bound public IP and for nodes behind full-cone
> NAT (most home routers).
> you'd rather not pin it in config. Note: STUN observes the
> reflexive IP from an ephemeral socket, then pairs it with
> the listener's bind port for the advert — the advert is
> only useful if your listener really is reachable at that
> public IP/port, which the daemon can't tell from STUN
> alone. A manual probe from a second host is the only sure
> check.
> - `external_addr: "<ip>[:<port>]"` — explicit override.
> Right when you already know your public IP — a static
> residential IP, an Elastic IP behind 1:1 NAT, a cloud
@@ -86,8 +91,9 @@ port to put in the advert:
> to the public IP returns `EADDRNOTAVAIL`.
>
> If you bind UDP to a specific public IP rather than
> `0.0.0.0`, neither flag is needed — the daemon advertises
> whatever it's bound to.
> `0.0.0.0`, neither STUN nor `external_addr` is needed — but
> `advertise_on_nostr: true` and `public: true` are still both
> required for the daemon to publish the endpoint.
Adverts don't sit on the relays forever:
@@ -167,14 +173,17 @@ transports:
udp:
bind_addr: "0.0.0.0:2121"
advertise_on_nostr: true
public: true
external_addr: "203.0.113.45:2121"
```
Replace `203.0.113.45:2121` with your actual public IP and
port. The bare-IP form `external_addr: "203.0.113.45"` is also
accepted; the daemon combines it with the bind port. You may
set both `public: true` and `external_addr` together — the
explicit override wins, with STUN as a logging cross-check.
accepted; the daemon combines it with the bind port. `public:
true` is still required as the master switch that gates UDP
advertisement; setting `external_addr` alongside it wins, and
STUN auto-discovery is skipped entirely (no logging
cross-check).
`advertise_on_nostr: true` is the bit that says "include this
transport in my published advert" — common to both paths.
@@ -191,8 +200,11 @@ sudo systemctl status fips
Status should show `active (running)`. Within a few seconds the
daemon will:
1. Run a one-shot STUN observation against the default STUN
servers to learn its public IP.
1. Determine the address to advertise. If you set `external_addr`,
the daemon uses it directly and skips STUN. If you set only
`public: true`, the daemon runs a one-shot STUN observation
against the default STUN servers and uses the reflexive IPv4 it
learns.
2. Build a Kind 37195 advert listing
`udp:<public-ip>:2121` (and any other transports you have
`advertise_on_nostr: true` on).
@@ -200,12 +212,13 @@ daemon will:
4. Publish it to the three default advert relays.
5. Schedule a refresh every 30 minutes.
If STUN fails (for example, if the network blocks outbound
UDP/3478), the daemon emits a WARN line in the journal and
suppresses the UDP entry from the advert rather than publishing
a wrong address. The link to `test-us01` from the previous
tutorial keeps working regardless — only the publish side is
gated on STUN.
If you took the `public: true` path and STUN fails (for example,
the network blocks outbound UDP/3478), the daemon emits a WARN
line in the journal and suppresses the UDP entry from the advert
rather than publishing a wrong address. The link to `test-us01`
from the previous tutorial keeps working regardless — only the
publish side is gated on STUN, and only on the STUN path. The
`external_addr` path doesn't depend on STUN reachability at all.
Quick sanity check on the journal:
@@ -295,22 +308,24 @@ verifiable in Step 4.
to them specifically or not. The test mesh's open-discovery
nodes will pick you up automatically.
## If you're behind symmetric NAT
## If your direct UDP advert isn't reachable
`public: true` + STUN works on most home and office NATs (the
full-cone variety) and on nodes with a directly-bound public
IP. It does *not* work on symmetric NAT, where the NAT mapping
is keyed on (source-port, destination-host) so the IP/port
your STUN server saw isn't the IP/port a different peer would
see.
`public: true` advertises the IP STUN observes paired with your
listener's bind port. That advert is only useful if your listener
really is reachable at that public IP/port — STUN can confirm the
public IP but not that an unsolicited inbound packet to the bind
port will make it through. The most common cause of the listener
being unreachable is symmetric NAT (where the public port a peer
sees varies per remote endpoint), but other configurations can
have the same effect.
For symmetric-NAT networks the alternative is `udp:nat` mode,
which advertises a placeholder `udp:nat` endpoint along with
the daemon's signaling-relay and STUN-server lists, and
performs UDP hole-punching at dial time. Both sides need to be
running matching configs and at least one side needs a
non-symmetric NAT for the punch to succeed; symmetric on both
sides is not reliably traversable and will time out.
When direct UDP advertising can't be relied on, the alternative
is `udp:nat` mode, which advertises a placeholder `udp:nat`
endpoint along with the daemon's signaling-relay and STUN-server
lists, and performs UDP hole-punching at dial time. Hole-punching
is best-effort — it works reliably when both sides are full-cone
or port-restricted, and symmetric NAT on either side typically
defeats it. Both sides need matching configs.
The minimal config switch:
@@ -339,7 +354,7 @@ discovery:
For the full setup including peer-side config and the punch-
duration knob, see
[../how-to/enable-nostr-discovery.md § Capability 2c](../how-to/enable-nostr-discovery.md#sub-scenario-2c-udp-hole-punching-for-nodes-behind-nat).
[../how-to/enable-nostr-discovery.md § When the node is behind NAT](../how-to/enable-nostr-discovery.md#when-the-node-is-behind-nat).
Separately from NAT considerations, FIPS supports running a
node behind a Tor onion service as a deployment shape in its
@@ -347,7 +362,7 @@ own right — chosen for the privacy, anonymity, and
censorship-resistance properties it brings, not as a fallback
when UDP or TCP fail. If those properties are an independent
goal for your node, see
[../how-to/enable-nostr-discovery.md § Sub-scenario 2b](../how-to/enable-nostr-discovery.md#sub-scenario-2b-tor-onion-node)
[../how-to/enable-nostr-discovery.md § Tor onion node](../how-to/enable-nostr-discovery.md#tor-onion-node)
and
[../how-to/deploy-tor-onion.md](../how-to/deploy-tor-onion.md).
@@ -364,10 +379,12 @@ If your advert doesn't appear on the relays:
with a non-public address (e.g., `10.x.x.x` or
`192.168.x.x`), STUN didn't see your real public IP — likely
you're behind a CGNAT that NATs your STUN traffic too, or a
corporate firewall that proxies it. Switch to the
`external_addr` form from Step 2 with your actual public
IP, or replace `public: true` with the bound interface IP
directly under `bind_addr`.
corporate firewall that proxies it. Two correct fixes:
(a) keep `public: true` and add `external_addr: <your-IP>`
(the explicit override wins and skips STUN); or (b) bind
directly to your public interface
(`bind_addr: <pub-ip>:2121`) and keep `advertise_on_nostr:
true` and `public: true`. Don't drop those flags.
- **Relay reachability.** `nak req` against a relay you can
reach but no events return — possibly the publish failed
@@ -379,9 +396,14 @@ If your advert doesn't appear on the relays:
nak req ... wss://offchain.pub
```
- **`advertise_on_nostr` typo.** YAML is case-sensitive and
silently ignores unknown keys. If `nak` returns no advert at
all, double-check the spelling on the UDP block and that
- **`advertise_on_nostr` typo.** YAML is case-sensitive. The
config parser rejects unknown keys via
`serde(deny_unknown_fields)` on the per-section structs, so a
misspelled field will refuse the daemon's start with a
parse-error line in the journal naming the unknown field.
If the daemon is running but `nak` returns no advert, the
field was accepted but something else is wrong; double-check
the spelling on the UDP block and that
`discovery.nostr.advertise: true` is also set.
## What's next
+12 -7
View File
@@ -110,8 +110,12 @@ You should be coming out of
[advertise-your-node](advertise-your-node.md) with:
- Persistent identity, advertising enabled
(`discovery.nostr.advertise: true`), UDP advertising on
Nostr (`transports.udp.advertise_on_nostr: true`).
(`discovery.nostr.advertise: true`), and either the
direct-UDP path
(`transports.udp.advertise_on_nostr: true`,
`transports.udp.public: true`) or the `udp:nat` path
(`transports.udp.advertise_on_nostr: true`,
`transports.udp.public: false`) from the previous tutorial.
- A static `test-us01` peer entry that the daemon dials
outbound; possibly an inbound `test-us03` peer (the
open-discovery test mesh node that dialed in after seeing
@@ -288,11 +292,12 @@ the previous tutorial:
WebSocket connection to the relays is failing repeatedly,
no adverts arrive. Look for relay-connection errors in
`sudo journalctl -u fips -n 200`.
- **`policy: open` typo.** YAML accepts and ignores unknown
values silently. If `fipsctl show status` (or the daemon's
startup log) shows `policy: configured_only`, the YAML
didn't parse the new value — re-check spelling and
indentation.
- **`policy: open` typo.** YAML is case-sensitive, and the
`policy` field is a serde enum that rejects unknown values —
a misspelled value produces a config-parse error at startup
rather than a silent fall-back. If the daemon refuses to
start, check `sudo journalctl -u fips -n 200` for the
parse-error line naming the field and value.
If too many peers are appearing and you want to dial down:
+7 -5
View File
@@ -125,9 +125,10 @@ Two knobs, one job each:
`wss://offchain.pub`) and is now able to query and consume
adverts.
- `advertise: false` keeps the publish side off. Your daemon
will not publish an advert of its own at this stage. (This
is the default, but it is good practice to make the choice
explicit while you're learning.)
will not publish an advert of its own at this stage. The
default is `true`, so we are setting it explicitly to
disable advertising for this consume-only tutorial. The next
tutorial flips it back on.
## Step 3: Switch the peer entry to `via_nostr`
@@ -270,8 +271,9 @@ If the link does not come up:
[advertise-your-node](advertise-your-node.md) publishes your
daemon's UDP endpoint on Nostr so other operators can add
you to their `peers:` list with `via_nostr: true` and reach
you the way you just reached `test-us01`. Includes a section
on `udp:nat` NAT traversal for symmetric-NAT networks.
you the way you just reached `test-us01`. Includes a short
final section on `udp:nat`, the best-effort hole-punching
path for nodes without a directly reachable UDP endpoint.
- **Discover peers with no prior configuration.**
[open-discovery](open-discovery.md) switches your daemon to
+1 -1
View File
@@ -10,7 +10,7 @@ WORKDIR /build
COPY Cargo.toml Cargo.lock rust-toolchain.toml build.rs ./
COPY src ./src
RUN cargo build --release --no-default-features --features tui && \
RUN cargo build --release && \
cp target/release/fips target/release/fipsctl target/release/fipstop /usr/local/bin/
# ── Runtime stage ──
+3 -1
View File
@@ -10,7 +10,7 @@ depends=('gcc-libs' 'glibc')
makedepends=('cargo')
optdepends=('systemd-resolved: .fips DNS resolution')
conflicts=('fips-git')
backup=('etc/fips/fips.yaml' 'etc/fips/hosts')
backup=('etc/fips/fips.yaml' 'etc/fips/hosts' 'etc/fips/fips.nft')
install=fips.install
source=("$pkgname-$pkgver.tar.gz::https://github.com/jmcorgan/fips/archive/v$pkgver.tar.gz"
"fips.sysusers"
@@ -51,10 +51,12 @@ package() {
install -Dm0644 packaging/debian/fips.service "$pkgdir/usr/lib/systemd/system/fips.service"
install -Dm0644 packaging/debian/fips-dns.service "$pkgdir/usr/lib/systemd/system/fips-dns.service"
install -Dm0644 packaging/debian/fips-gateway.service "$pkgdir/usr/lib/systemd/system/fips-gateway.service"
install -Dm0644 packaging/debian/fips-firewall.service "$pkgdir/usr/lib/systemd/system/fips-firewall.service"
# Config files (from packaging/common/)
install -Dm0600 packaging/common/fips.yaml "$pkgdir/etc/fips/fips.yaml"
install -Dm0644 packaging/common/hosts "$pkgdir/etc/fips/hosts"
install -Dm0644 packaging/common/fips.nft "$pkgdir/etc/fips/fips.nft"
# System integration (from local source files in $srcdir)
install -Dm0644 "$srcdir/fips.sysusers" "$pkgdir/usr/lib/sysusers.d/fips.conf"
+5 -1
View File
@@ -11,7 +11,7 @@ makedepends=('cargo' 'git')
optdepends=('systemd-resolved: .fips DNS resolution')
provides=('fips')
conflicts=('fips')
backup=('etc/fips/fips.yaml' 'etc/fips/hosts')
backup=('etc/fips/fips.yaml' 'etc/fips/hosts' 'etc/fips/fips.nft')
install=fips.install
source=("fips::git+https://github.com/jmcorgan/fips.git"
"fips.sysusers"
@@ -52,14 +52,18 @@ package() {
install -Dm0755 target/release/fips "$pkgdir/usr/bin/fips"
install -Dm0755 target/release/fipsctl "$pkgdir/usr/bin/fipsctl"
install -Dm0755 target/release/fipstop "$pkgdir/usr/bin/fipstop"
install -Dm0755 target/release/fips-gateway "$pkgdir/usr/bin/fips-gateway"
# Systemd service files (from packaging/debian/ -- correct /usr/bin/ paths)
install -Dm0644 packaging/debian/fips.service "$pkgdir/usr/lib/systemd/system/fips.service"
install -Dm0644 packaging/debian/fips-dns.service "$pkgdir/usr/lib/systemd/system/fips-dns.service"
install -Dm0644 packaging/debian/fips-gateway.service "$pkgdir/usr/lib/systemd/system/fips-gateway.service"
install -Dm0644 packaging/debian/fips-firewall.service "$pkgdir/usr/lib/systemd/system/fips-firewall.service"
# Config files (from packaging/common/)
install -Dm0600 packaging/common/fips.yaml "$pkgdir/etc/fips/fips.yaml"
install -Dm0644 packaging/common/hosts "$pkgdir/etc/fips/hosts"
install -Dm0644 packaging/common/fips.nft "$pkgdir/etc/fips/fips.nft"
# System integration (from local source files in $srcdir)
install -Dm0644 "$srcdir/fips.sysusers" "$pkgdir/usr/lib/sysusers.d/fips.conf"
+16
View File
@@ -23,6 +23,22 @@ Both PKGBUILDs reference files from `packaging/debian/` (service files) and
`packaging/common/` (config files) at build time. These are pulled from the
source tree during `package()`, not from this directory.
### What Gets Installed
Both PKGBUILDs install the same payload, kept at parity with the Debian
package:
- Binaries: `fips`, `fipsctl`, `fipstop`, `fips-gateway`
- Systemd units: `fips.service`, `fips-dns.service`, `fips-gateway.service`,
`fips-firewall.service`
- Config: `/etc/fips/fips.yaml`, `/etc/fips/hosts`, `/etc/fips/fips.nft`
- sysusers/tmpfiles fragments for the `fips` group and `/run/fips/`
The `fips.nft` baseline is shipped as a conffile (listed in `backup=()`) so
operator edits to the nftables ruleset survive package upgrades.
`fips-firewall.service` is shipped disabled by default, matching the Debian
package: operators opt in by enabling it explicitly.
## Local Build and Validation
Build and validate the `-git` package locally using the Makefile target:
+83 -14
View File
@@ -15,12 +15,24 @@ sudo ./install.sh
| fips (daemon) | /usr/local/bin/fips |
| fipsctl (CLI) | /usr/local/bin/fipsctl |
| fipstop (TUI) | /usr/local/bin/fipstop |
| fips-gateway (LAN bridge) | /usr/local/bin/fips-gateway |
| Configuration | /etc/fips/fips.yaml |
| Identity key | /etc/fips/fips.key (auto-generated) |
| Public key | /etc/fips/fips.pub (auto-generated) |
| systemd unit | /etc/systemd/system/fips.service |
| Hosts file | /etc/fips/hosts |
| Firewall baseline | /etc/fips/fips.nft |
| Firewall drop-in directory | /etc/fips/fips.d/ |
| Daemon unit | /etc/systemd/system/fips.service (enabled) |
| DNS routing unit | /etc/systemd/system/fips-dns.service (enabled) |
| Gateway unit | /etc/systemd/system/fips-gateway.service (NOT enabled) |
| Firewall unit | /etc/systemd/system/fips-firewall.service (NOT enabled) |
| DNS helpers | /usr/lib/fips/fips-dns-{setup,teardown} |
A system group `fips` is created for control socket access.
A system group `fips` is created for control socket access. By
default, only `fips.service` and `fips-dns.service` are enabled at
install time. `fips-gateway.service` and `fips-firewall.service`
are installed but require explicit operator opt-in (see the
sections below).
## Post-Install Configuration
@@ -101,26 +113,69 @@ peers:
connect_policy: auto_connect
```
### 5. DNS Resolver (optional, requires systemd-resolved)
### 5. DNS Resolver
FIPS includes a DNS responder for `.fips` domain names (port 5354).
On systems running `systemd-resolved`, the installer automatically enables
`fips-dns.service` to route `.fips` queries to the FIPS resolver.
FIPS includes a DNS responder for `.fips` domain names that listens on
`fips0` and on `[::1]:5354`. The `fips-dns.service` helper detects the
host's DNS routing system and configures it to forward `.fips` queries
to the responder. Backends tried in order:
If `systemd-resolved` is not running at install time, DNS integration is
skipped. To enable it later (after starting `systemd-resolved`):
1. systemd `dns-delegate` drop-in (systemd >= 258, declarative)
2. `systemd-resolved` global drop-in via `/etc/systemd/resolved.conf.d/`
3. `systemd-resolved` per-link `resolvectl` (legacy fallback)
4. `dnsmasq` (standalone, drops a config in `/etc/dnsmasq.d/`)
5. NetworkManager with the `dnsmasq` plugin
If none of the supported backends is detected, `fips-dns-setup` logs a
warning with manual instructions and exits cleanly. The daemon itself
keeps working; only the host's `.fips` resolution is left unwired.
The installer enables `fips-dns.service` automatically. To disable
or re-enable later:
```bash
sudo systemctl enable --now fips-dns.service
sudo systemctl disable --now fips-dns.service # disable
sudo systemctl enable --now fips-dns.service # re-enable
```
For manual configuration without `fips-dns.service`:
### 6. Mesh-interface firewall baseline (optional)
`fips.nft` is a default-deny baseline for inbound traffic on the
`fips0` mesh interface. It is shipped as `/etc/fips/fips.nft` (not
loaded by default) along with a disabled `fips-firewall.service`
unit. Enable it explicitly:
```bash
sudo resolvectl dns fips0 127.0.0.1:5354
sudo resolvectl domain fips0 ~fips
sudo systemctl enable --now fips-firewall.service
```
The baseline polices only `fips0`, leaving Docker, Tor, the host
firewall, and other interfaces untouched. Outbound from `fips0` is
unrestricted; inbound is dropped except for replies to outbound
flows, ICMPv6 echo-request, and any operator drop-ins under
`/etc/fips/fips.d/*.nft`. Read the comments at the top of
`/etc/fips/fips.nft` for the full policy and how to add per-service
allow rules.
### 7. Outbound LAN gateway (optional)
`fips-gateway` bridges unmodified LAN hosts to `.fips` destinations
through a DNS-allocated virtual IPv6 pool and kernel nftables NAT.
The binary is installed at `/usr/local/bin/fips-gateway` and a
`fips-gateway.service` unit ships disabled by default.
To enable it, configure the gateway block in `/etc/fips/fips.yaml`,
then:
```bash
sudo systemctl enable --now fips-gateway.service
```
The unit `Requires=fips.service`, waits up to 30 seconds for `fips0`
to come up, and runs `fips-gateway --config /etc/fips/fips.yaml`.
Inbound port-forward rules can be added in the same `gateway:`
block.
## Firewall Ports
| Port | Protocol | Purpose |
@@ -130,14 +185,28 @@ sudo resolvectl domain fips0 ~fips
## Service Management
The install ships four units. `fips.service` and `fips-dns.service`
are enabled at install time. `fips-gateway.service` and
`fips-firewall.service` are installed but disabled until the
operator opts in.
```bash
# Start / stop / restart
# Daemon
sudo systemctl start fips
sudo systemctl stop fips
sudo systemctl restart fips
# View logs
# DNS routing helper
sudo systemctl restart fips-dns
# Optional services (opt-in)
sudo systemctl enable --now fips-firewall # mesh-interface nftables baseline
sudo systemctl enable --now fips-gateway # outbound LAN gateway
# View logs (any of the units above)
sudo journalctl -u fips -f
sudo journalctl -u fips-gateway -f
sudo journalctl -u fips-firewall -f
# Switch to debug logging
sudo systemctl set-environment RUST_LOG=debug
+11 -2
View File
@@ -98,7 +98,7 @@ rm -rf "${STAGING_DIR}"
mkdir -p "${STAGING_DIR}"
# Copy binaries
for bin in fips fipsctl fipstop; do
for bin in fips fipsctl fipstop fips-gateway; do
if [[ ! -f "${BINARY_DIR}/${bin}" ]]; then
echo "Missing binary: ${BINARY_DIR}/${bin}" >&2
exit 1
@@ -111,15 +111,24 @@ if ! command -v "${STRIP_BIN}" &>/dev/null; then
echo "Strip tool not found: ${STRIP_BIN}" >&2
exit 1
fi
"${STRIP_BIN}" "${STAGING_DIR}/fips" "${STAGING_DIR}/fipsctl" "${STAGING_DIR}/fipstop"
"${STRIP_BIN}" \
"${STAGING_DIR}/fips" \
"${STAGING_DIR}/fipsctl" \
"${STAGING_DIR}/fipstop" \
"${STAGING_DIR}/fips-gateway"
# Copy packaging files
cp "${SCRIPT_DIR}/install.sh" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/uninstall.sh" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/fips.service" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/fips-dns.service" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/fips-gateway.service" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/fips-firewall.service" "${STAGING_DIR}/"
cp "${PACKAGING_DIR}/common/fips.yaml" "${STAGING_DIR}/"
cp "${PACKAGING_DIR}/common/hosts" "${STAGING_DIR}/"
cp "${PACKAGING_DIR}/common/fips.nft" "${STAGING_DIR}/"
cp "${PACKAGING_DIR}/common/fips-dns-setup" "${STAGING_DIR}/"
cp "${PACKAGING_DIR}/common/fips-dns-teardown" "${STAGING_DIR}/"
cp "${SCRIPT_DIR}/README.install.md" "${STAGING_DIR}/"
chmod +x "${STAGING_DIR}/install.sh" "${STAGING_DIR}/uninstall.sh"
+17
View File
@@ -0,0 +1,17 @@
# Firewall is not enabled by default. Enable with: systemctl enable --now fips-firewall
[Unit]
Description=FIPS mesh-interface nftables baseline
Documentation=file:///etc/fips/fips.nft
Before=fips.service
ConditionPathExists=/etc/fips/fips.nft
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/nft -f /etc/fips/fips.nft
ExecStop=-/usr/sbin/nft delete table inet fips
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+33
View File
@@ -0,0 +1,33 @@
# Gateway is not enabled by default. Enable with: systemctl enable --now fips-gateway
[Unit]
Description=FIPS Outbound LAN Gateway
After=fips.service
Requires=fips.service
[Service]
Type=simple
# Wait up to 30s for the fips daemon's TUN to come up before launching
# the gateway. fips-gateway hard-fails on a missing fips0 interface; with
# Requires=fips.service the daemon unit is started, but the TUN device
# itself is brought up some time after the daemon process starts. Without
# this wait the gateway's first ExecStart tends to lose a race on fresh
# boot, exit, and rely on Restart=on-failure for ~5s of recovery. Bounded
# wait avoids the noisy restart cycle while still surfacing real failures
# (if fips0 never appears, the wait expires and the gateway's existing
# error path runs).
ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do ip link show fips0 >/dev/null 2>&1 && exit 0; sleep 1; done; echo "fips0 did not appear within 30s" >&2; exit 1'
ExecStart=/usr/local/bin/fips-gateway --config /etc/fips/fips.yaml
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
TimeoutStopSec=15
# Security hardening (needs CAP_NET_ADMIN for nftables and proxy NDP)
ProtectHome=yes
PrivateTmp=yes
ProtectKernelModules=yes
ProtectKernelTunables=no
[Install]
WantedBy=multi-user.target
+57 -5
View File
@@ -9,10 +9,15 @@
# /usr/local/bin/fips Daemon binary
# /usr/local/bin/fipsctl CLI query tool
# /usr/local/bin/fipstop TUI monitor
# /usr/local/bin/fips-gateway Outbound LAN gateway binary (opt-in)
# /etc/fips/fips.yaml Configuration (preserved if exists)
# /etc/fips/hosts Host-to-npub mappings (preserved if exists)
# /etc/systemd/system/fips.service systemd unit
# /etc/systemd/system/fips-dns.service DNS routing for .fips domain
# /etc/fips/fips.nft Mesh-interface nftables baseline (preserved if exists)
# /etc/fips/fips.d/ Operator drop-in directory for nft rules
# /etc/systemd/system/fips.service Daemon unit (enabled)
# /etc/systemd/system/fips-dns.service DNS routing for .fips domain (enabled)
# /etc/systemd/system/fips-gateway.service Gateway unit (NOT enabled; opt-in)
# /etc/systemd/system/fips-firewall.service Firewall baseline unit (NOT enabled; opt-in)
set -euo pipefail
@@ -60,6 +65,9 @@ install -m 0755 "${SCRIPT_DIR}/fipsctl" "${INSTALL_PREFIX}/bin/fipsctl"
if [ -f "${SCRIPT_DIR}/fipstop" ]; then
install -m 0755 "${SCRIPT_DIR}/fipstop" "${INSTALL_PREFIX}/bin/fipstop"
fi
if [ -f "${SCRIPT_DIR}/fips-gateway" ]; then
install -m 0755 "${SCRIPT_DIR}/fips-gateway" "${INSTALL_PREFIX}/bin/fips-gateway"
fi
# --- Install configuration ---
@@ -82,7 +90,27 @@ else
echo "Hosts file installed to ${HOSTS_FILE}"
fi
# --- Install systemd unit ---
# Mesh-interface nftables baseline. Preserved on upgrade like fips.yaml
# so operator edits aren't clobbered.
NFT_FILE="${CONFIG_DIR}/fips.nft"
if [ -f "${NFT_FILE}" ]; then
echo "Firewall baseline exists at ${NFT_FILE}, not overwriting."
install -m 0644 "${SCRIPT_DIR}/fips.nft" "${CONFIG_DIR}/fips.nft.template"
echo " New template installed as ${CONFIG_DIR}/fips.nft.template"
elif [ -f "${SCRIPT_DIR}/fips.nft" ]; then
install -m 0644 "${SCRIPT_DIR}/fips.nft" "${NFT_FILE}"
echo "Firewall baseline installed to ${NFT_FILE}"
fi
# Drop-in directory for operator nftables rules included by
# /etc/fips/fips.nft. Empty by default; the include glob matches
# nothing cleanly out of the box.
if [ ! -d "${CONFIG_DIR}/fips.d" ]; then
install -d -m 0755 "${CONFIG_DIR}/fips.d"
echo "Drop-in directory created at ${CONFIG_DIR}/fips.d/"
fi
# --- Install systemd units ---
was_active=false
if systemctl is-active --quiet fips.service 2>/dev/null; then
@@ -100,9 +128,23 @@ fi
install -m 0644 "${SCRIPT_DIR}/fips.service" "${SYSTEMD_DIR}/fips.service"
install -m 0644 "${SCRIPT_DIR}/fips-dns.service" "${SYSTEMD_DIR}/fips-dns.service"
if [ -f "${SCRIPT_DIR}/fips-gateway.service" ]; then
install -m 0644 "${SCRIPT_DIR}/fips-gateway.service" "${SYSTEMD_DIR}/fips-gateway.service"
fi
if [ -f "${SCRIPT_DIR}/fips-firewall.service" ]; then
install -m 0644 "${SCRIPT_DIR}/fips-firewall.service" "${SYSTEMD_DIR}/fips-firewall.service"
fi
# DNS helpers ship flat in the tarball alongside install.sh; from a
# source checkout they live under packaging/common/. Resolve from
# either layout.
install -d -m 0755 /usr/lib/fips
install -m 0755 "${SCRIPT_DIR}/../common/fips-dns-setup" /usr/lib/fips/fips-dns-setup
install -m 0755 "${SCRIPT_DIR}/../common/fips-dns-teardown" /usr/lib/fips/fips-dns-teardown
if [ -f "${SCRIPT_DIR}/fips-dns-setup" ]; then
install -m 0755 "${SCRIPT_DIR}/fips-dns-setup" /usr/lib/fips/fips-dns-setup
install -m 0755 "${SCRIPT_DIR}/fips-dns-teardown" /usr/lib/fips/fips-dns-teardown
else
install -m 0755 "${SCRIPT_DIR}/../common/fips-dns-setup" /usr/lib/fips/fips-dns-setup
install -m 0755 "${SCRIPT_DIR}/../common/fips-dns-teardown" /usr/lib/fips/fips-dns-teardown
fi
systemctl daemon-reload
echo "systemd units and DNS scripts installed."
@@ -149,6 +191,16 @@ echo ""
echo "Start the service:"
echo " sudo systemctl start fips"
echo ""
echo "Optional services (NOT enabled by default):"
echo ""
echo " Mesh-interface firewall baseline (default-deny on fips0):"
echo " sudo systemctl enable --now fips-firewall.service"
echo " Operator drop-ins under /etc/fips/fips.d/*.nft"
echo ""
echo " Outbound LAN gateway (bridge unmodified LAN hosts to .fips):"
echo " sudo systemctl enable --now fips-gateway.service"
echo " Configure under the gateway: section of ${CONFIG_FILE}"
echo ""
echo "Monitor:"
echo " sudo journalctl -u fips -f"
echo " fipsctl show status"
+15 -19
View File
@@ -18,30 +18,26 @@ if [ "$(id -u)" -ne 0 ]; then
exit 1
fi
# --- Stop and disable service ---
# --- Stop and disable services ---
# Stop dependents (firewall, gateway, dns) before the daemon to avoid
# noisy "fips0 disappeared" cascades during the teardown.
if systemctl is-active --quiet fips-dns.service 2>/dev/null; then
echo "Stopping fips-dns service..."
systemctl stop fips-dns.service
fi
if systemctl is-enabled --quiet fips-dns.service 2>/dev/null; then
systemctl disable fips-dns.service
fi
if systemctl is-active --quiet fips.service 2>/dev/null; then
echo "Stopping fips service..."
systemctl stop fips.service
fi
if systemctl is-enabled --quiet fips.service 2>/dev/null; then
systemctl disable fips.service
fi
for unit in fips-gateway.service fips-firewall.service fips-dns.service fips.service; do
if systemctl is-active --quiet "${unit}" 2>/dev/null; then
echo "Stopping ${unit}..."
systemctl stop "${unit}"
fi
if systemctl is-enabled --quiet "${unit}" 2>/dev/null; then
systemctl disable "${unit}"
fi
done
# --- Remove systemd units ---
rm -f /etc/systemd/system/fips.service
rm -f /etc/systemd/system/fips-dns.service
rm -f /etc/systemd/system/fips-gateway.service
rm -f /etc/systemd/system/fips-firewall.service
rm -rf /usr/lib/fips/
systemctl daemon-reload
echo "systemd units and DNS scripts removed."
@@ -57,7 +53,7 @@ rm -f /etc/tmpfiles.d/fips.conf
# --- Remove binaries ---
rm -f /usr/local/bin/fips /usr/local/bin/fipsctl /usr/local/bin/fipstop
rm -f /usr/local/bin/fips /usr/local/bin/fipsctl /usr/local/bin/fipstop /usr/local/bin/fips-gateway
echo "Binaries removed."
# --- Optionally remove configuration and group ---
@@ -1,6 +1,6 @@
# FIPS Tor test node A — socks5-outbound
#
# Connects outbound to vps-chi (217.77.8.91:443) via Tor SOCKS5 proxy.
# Connects outbound to test-us01 (217.77.8.91:443) via Tor SOCKS5 proxy.
# Identity generated per-run to avoid mesh clashes with parallel tests.
node:
@@ -21,7 +21,7 @@ transports:
peers:
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
alias: "vps-chi"
alias: "test-us01"
addresses:
- transport: tor
addr: "217.77.8.91:443"
@@ -1,6 +1,6 @@
# FIPS Tor test node B — socks5-outbound
#
# Connects outbound to vps-chi (217.77.8.91:443) via Tor SOCKS5 proxy.
# Connects outbound to test-us01 (217.77.8.91:443) via Tor SOCKS5 proxy.
# Identity generated per-run to avoid mesh clashes with parallel tests.
node:
@@ -21,7 +21,7 @@ transports:
peers:
- npub: "npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98"
alias: "vps-chi"
alias: "test-us01"
addresses:
- transport: tor
addr: "217.77.8.91:443"
@@ -1,10 +1,10 @@
# Tor transport integration test — socks5-outbound
#
# Topology:
# [fips-a] --tor/socks5--> vps-chi (217.77.8.91:443) <--tor/socks5-- [fips-b]
# [fips-a] --tor/socks5--> test-us01 (217.77.8.91:443) <--tor/socks5-- [fips-b]
#
# Both FIPS nodes connect outbound through a local Tor daemon's SOCKS5
# proxy to vps-chi's TCP listener. vps-chi routes between them.
# proxy to test-us01's TCP listener. test-us01 routes between them.
# Ping between fips-a and fips-b validates the full Tor transport path.
networks:
@@ -2,11 +2,11 @@
# Tor transport integration test.
#
# Validates end-to-end connectivity through a real Tor network:
# fips-a --tor/socks5--> vps-chi <--tor/socks5-- fips-b
# fips-a --tor/socks5--> test-us01 <--tor/socks5-- fips-b
#
# Both local FIPS nodes connect outbound through a local Tor daemon
# to vps-chi's TCP listener (217.77.8.91:443). Once both are peered
# with vps-chi, traffic between fips-a and fips-b is routed through it.
# to test-us01's TCP listener (217.77.8.91: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
# multiple instances of this test run concurrently.
@@ -107,7 +107,7 @@ fi
echo ""
# ── Phase 3: Wait for FIPS peers via Tor ─────────────────────────
echo "Phase 3: Waiting for FIPS nodes to peer with vps-chi via Tor (up to ${MAX_WAIT_PEER}s)..."
echo "Phase 3: Waiting for FIPS nodes to peer with test-us01 via Tor (up to ${MAX_WAIT_PEER}s)..."
echo " (First SOCKS5 attempt may timeout while Tor builds circuits)"
peers_a=0
@@ -199,7 +199,7 @@ print(f'{sum(trimmed)/len(trimmed):.1f}')
}
echo ""
echo " Ping via Tor (routed through vps-chi):"
echo " Ping via Tor (routed through test-us01):"
ping_series fips-tor-a "$NPUB_B" "A → B"
ping_series fips-tor-b "$NPUB_A" "B → A"