From d52d7debb709af4e00acbb3c55b26008be509a30 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 10 May 2026 21:52:33 +0000 Subject: [PATCH 1/5] aur: bring AUR packaging to .deb parity Three changes folded together close the AUR-side parity gap with the .deb packaging: - PKGBUILD now ships fips.nft baseline and fips-firewall.service, with fips.nft marked as backup so operator edits survive upgrades. - PKGBUILD-git mirrors the release PKGBUILD: installs fips-gateway (was missing entirely), ships fips.nft and fips-firewall.service, and tracks the same backup() set. - packaging/aur/README.md documents the gateway, firewall service, and nft baseline that ship as of this revision. AUR users now receive the same artifact set as .deb users. --- packaging/aur/PKGBUILD | 4 +++- packaging/aur/PKGBUILD-git | 6 +++++- packaging/aur/README.md | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packaging/aur/PKGBUILD b/packaging/aur/PKGBUILD index 84aead6..2f9e810 100644 --- a/packaging/aur/PKGBUILD +++ b/packaging/aur/PKGBUILD @@ -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" diff --git a/packaging/aur/PKGBUILD-git b/packaging/aur/PKGBUILD-git index 492b475..39dad6c 100644 --- a/packaging/aur/PKGBUILD-git +++ b/packaging/aur/PKGBUILD-git @@ -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" diff --git a/packaging/aur/README.md b/packaging/aur/README.md index 8da8bd4..4d51357 100644 --- a/packaging/aur/README.md +++ b/packaging/aur/README.md @@ -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: From eaba693b183b11c6fac5ec60b12f89112cdd11f0 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 10 May 2026 18:45:36 +0000 Subject: [PATCH 2/5] packaging: bring systemd tarball to feature parity with .deb / AUR The generic systemd install tarball is the catch-all install path for systemd Linux distros that don't have a per-format package (Fedora, RHEL/CentOS, openSUSE, Alpine, etc.). It had drifted behind the .deb and AUR packages and was missing fips-gateway, the mesh-interface firewall baseline, and the multi-backend DNS helper in the shipped tarball. Bring it to parity: - New `packaging/systemd/fips-gateway.service` (clone of the .deb unit; ExecStart pointed at `/usr/local/bin/fips-gateway`). Not enabled at install time; operator opt-in. - New `packaging/systemd/fips-firewall.service` (clone of the .deb unit; nft path unchanged at `/usr/sbin/nft`). Not enabled at install time; operator opt-in. - `build-tarball.sh` now bundles the `fips-gateway` binary, the two new units, the `fips.nft` baseline conffile, and the `fips-dns-setup` / `fips-dns-teardown` multi-backend helpers from `packaging/common/`. - `install.sh` now installs `fips-gateway` to `/usr/local/bin/`, installs both new units to `/etc/systemd/system/` (without enabling them), preserves `/etc/fips/fips.nft` on upgrade like `fips.yaml`, and creates the `/etc/fips/fips.d/` operator drop-in directory. Post-install messaging mentions both opt-in services. - `uninstall.sh` stops and disables the optional services in dependency order (firewall, gateway, dns, daemon), removes the new unit files, and removes the gateway binary. `--purge` already handles `/etc/fips/` removal which covers `fips.nft` and `fips.d/`. - `README.install.md` documents all of the above: expanded "What Gets Installed" table, new sections covering the firewall baseline and the LAN gateway, refreshed DNS section reflecting the multi-backend setup helper (systemd dns-delegate / systemd-resolved drop-in / per-link resolvectl / dnsmasq / NetworkManager-dnsmasq), and updated Service Management. Also fixes a latent packaging bug: `install.sh` previously referenced `${SCRIPT_DIR}/../common/fips-dns-setup`, a path that exists only in the source-repo layout and not in the extracted tarball. The script now resolves the helper from the staging directory first (the tarball case), falling back to the source-repo relative path. Bug latent since the multi-backend DNS helpers landed. CHANGELOG `[Unreleased]` documents the parity bump under Changed and the path-resolution fix under Fixed. Closes the longest-standing parity gap for non-Debian / non-Arch systemd Linux distros installing from the release-distribution tarball. --- CHANGELOG.md | 24 ++++++ packaging/systemd/README.install.md | 97 +++++++++++++++++++++---- packaging/systemd/build-tarball.sh | 13 +++- packaging/systemd/fips-firewall.service | 17 +++++ packaging/systemd/fips-gateway.service | 33 +++++++++ packaging/systemd/install.sh | 62 ++++++++++++++-- packaging/systemd/uninstall.sh | 34 ++++----- 7 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 packaging/systemd/fips-firewall.service create mode 100644 packaging/systemd/fips-gateway.service diff --git a/CHANGELOG.md b/CHANGELOG.md index 47ac6c7..283fbbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -388,9 +388,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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()` diff --git a/packaging/systemd/README.install.md b/packaging/systemd/README.install.md index 85ba088..9b2b924 100644 --- a/packaging/systemd/README.install.md +++ b/packaging/systemd/README.install.md @@ -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 diff --git a/packaging/systemd/build-tarball.sh b/packaging/systemd/build-tarball.sh index 7450cf0..fa76c20 100755 --- a/packaging/systemd/build-tarball.sh +++ b/packaging/systemd/build-tarball.sh @@ -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" diff --git a/packaging/systemd/fips-firewall.service b/packaging/systemd/fips-firewall.service new file mode 100644 index 0000000..76aaacd --- /dev/null +++ b/packaging/systemd/fips-firewall.service @@ -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 diff --git a/packaging/systemd/fips-gateway.service b/packaging/systemd/fips-gateway.service new file mode 100644 index 0000000..67167ad --- /dev/null +++ b/packaging/systemd/fips-gateway.service @@ -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 diff --git a/packaging/systemd/install.sh b/packaging/systemd/install.sh index dd96e6d..f71513b 100755 --- a/packaging/systemd/install.sh +++ b/packaging/systemd/install.sh @@ -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" diff --git a/packaging/systemd/uninstall.sh b/packaging/systemd/uninstall.sh index 364cdfc..251bb89 100755 --- a/packaging/systemd/uninstall.sh +++ b/packaging/systemd/uninstall.sh @@ -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 --- From 42b88c9bb8b98f32189243691458791d4b464b45 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 10 May 2026 21:53:40 +0000 Subject: [PATCH 3/5] docs: refresh README, CONTRIBUTING, and examples for v0.3.0 Four short prose corrections folded together to align operator-facing docs with current v0.3.0 reality: - README Rust prerequisite reconciled with the toolchain pin (1.94.1, was 1.85+). - CONTRIBUTING.md bumps the Rust prerequisite and adds the squash-merge policy note. - examples/sidecar-nostr-relay/Dockerfile drops stale --features tui and the paired --no-default-features; the tui/ble/gateway cargo features were replaced by platform cfg gates in cbc7809. - README Features list adds two v0.3.0-visible items: the mesh- interface security baseline (fips.nft conffile, fips.d/ drop-in, opt-in fips-firewall.service across all packaging formats) and the fipsctl stats time-series queries plus fipstop inline sparkline dashboards. Status badge bump (v0.3.0--dev to v0.3.0) is deferred to tag time per the release-prep checklist. --- CONTRIBUTING.md | 3 ++- README.md | 16 ++++++++++++---- examples/sidecar-nostr-relay/Dockerfile | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index faefb1b..f7e5f87 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 941d7b0..e071968 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/examples/sidecar-nostr-relay/Dockerfile b/examples/sidecar-nostr-relay/Dockerfile index 5700d02..0e58126 100644 --- a/examples/sidecar-nostr-relay/Dockerfile +++ b/examples/sidecar-nostr-relay/Dockerfile @@ -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 ── From 77fdd52fe0f14952fb1d0f34e2616a95010bf3c1 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 10 May 2026 21:56:09 +0000 Subject: [PATCH 4/5] docs: reviewer feedback pass on Nostr-discovery surface Walk through reviewer feedback on the Nostr-discovery docs and land 18 items. Bulk patterns: - `external_addr` / `public: true` semantics consistently misdescribed. The advert path is gated on `cfg.is_public()`; inside that branch the daemon picks an address by precedence (`external_addr`, non-wildcard `bind_addr`, STUN). The docs treated `public: true` and `external_addr` as alternatives when they are stacked: `public: true` is the master switch and `external_addr` populates the address inside it. Reconciled across `enable-nostr-discovery.md` and `advertise-your-node.md`: add `public: true` to the `external_addr` examples; replace "STUN as a logging cross-check" with "STUN is skipped entirely"; fix "neither flag is needed" for direct public bind (both flags still required); make the publish-tutorial Step 3 conditional on the chosen Step 2 path (STUN runs only on the `public: true` path); rewrite the troubleshooting "wrong public IP advertised" bullet with two coherent fixes. - `udp:nat` overpromised as a symmetric-NAT solution. Symmetric NAT on either side typically defeats the punch. Reframe `udp:nat` as best-effort hole-punching for nodes without a directly reachable UDP endpoint in the how-to, the publish tutorial (intro, callout, section heading rewrite from "If you're behind symmetric NAT" to "If your direct UDP advert isn't reachable"), the consume tutorial's "What's next" pointer, and `tutorials/README.md`. Promote reachability over named NAT classes: STUN can confirm the public IP but not that the listener-port mapping is open. - YAML "silently ignores unknown keys" is wrong. Config parser rejects unknown fields via `serde(deny_unknown_fields)` on the per-section structs; misspelled fields refuse the daemon's start with a parse-error line in the journal. Fixed in the publish tutorial's troubleshooting and the open-discovery tutorial's `policy` typo bullet. Mechanical fixes: - Repoint stale anchors. `getting-started.md` and `configuration.md` linked to `#installation` / `#inspect` on the README; the README has no such headings. Repoint to `#quick-start` and `cli-fipsctl.md`. Two stale anchors in the publish tutorial pointing at non-existent sub-scenarios in the how-to (`#sub-scenario-2c-...`, `#sub-scenario-2b-tor-onion-node`) repointed to the correct anchors. - Drop the `fipsctl show status` claim from the open-discovery troubleshooting bullet (`show_status` doesn't include `discovery.nostr.policy`). Replace with daemon startup logs. - Fix the `advertise: false` parenthetical in the consume-only tutorial (`default_advertise()` returns `true`; we set `false` explicitly for the consume-only path). - Drop the "supplies a relay list" overstatement in two activation paragraphs (the how-to and the design doc). Default relay / STUN-server lists ship in the config; both are optional overrides. - Add the missing `transports.udp.public` entry to the open-discovery tutorial's prerequisites checklist. Tutorial users coming out of advertise-your-node could be on either the direct-UDP (`public: true`) or `udp:nat` (`public: false`) path; list both. Files: docs/getting-started.md, docs/reference/configuration.md, docs/how-to/enable-nostr-discovery.md, docs/tutorials/README.md, docs/tutorials/advertise-your-node.md, docs/tutorials/resolve-peers-via-nostr.md, docs/tutorials/open-discovery.md, docs/design/fips-nostr-discovery.md. --- docs/design/fips-nostr-discovery.md | 5 +- docs/getting-started.md | 9 +- docs/how-to/enable-nostr-discovery.md | 34 ++++--- docs/reference/configuration.md | 2 +- docs/tutorials/README.md | 2 +- docs/tutorials/advertise-your-node.md | 104 +++++++++++++--------- docs/tutorials/open-discovery.md | 19 ++-- docs/tutorials/resolve-peers-via-nostr.md | 12 +-- 8 files changed, 116 insertions(+), 71 deletions(-) diff --git a/docs/design/fips-nostr-discovery.md b/docs/design/fips-nostr-discovery.md index e4553fd..df65088 100644 --- a/docs/design/fips-nostr-discovery.md +++ b/docs/design/fips-nostr-discovery.md @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md index 3ac2e50..f7e4152 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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 diff --git a/docs/how-to/enable-nostr-discovery.md b/docs/how-to/enable-nostr-discovery.md index 1ad5deb..2e91117 100644 --- a/docs/how-to/enable-nostr-discovery.md +++ b/docs/how-to/enable-nostr-discovery.md @@ -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::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 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 761717d..a8fd3ed 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -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 diff --git a/docs/tutorials/README.md b/docs/tutorials/README.md index 25d136f..8487982 100644 --- a/docs/tutorials/README.md +++ b/docs/tutorials/README.md @@ -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 `.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. | diff --git a/docs/tutorials/advertise-your-node.md b/docs/tutorials/advertise-your-node.md index abc8188..cb25f6f 100644 --- a/docs/tutorials/advertise-your-node.md +++ b/docs/tutorials/advertise-your-node.md @@ -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: "[:]"` — 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::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: ` + (the explicit override wins and skips STUN); or (b) bind + directly to your public interface + (`bind_addr: :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 diff --git a/docs/tutorials/open-discovery.md b/docs/tutorials/open-discovery.md index e17a63d..a6d1f9a 100644 --- a/docs/tutorials/open-discovery.md +++ b/docs/tutorials/open-discovery.md @@ -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: diff --git a/docs/tutorials/resolve-peers-via-nostr.md b/docs/tutorials/resolve-peers-via-nostr.md index 3b887aa..284088d 100644 --- a/docs/tutorials/resolve-peers-via-nostr.md +++ b/docs/tutorials/resolve-peers-via-nostr.md @@ -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 From 0e57216d98c756fddb8ba0784ffb3d6bb2e61be2 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 10 May 2026 16:33:05 +0000 Subject: [PATCH 5/5] testing: rename vps-chi to test-us01 in tor socks5-outbound scaffold The host (217.77.8.91:443) was renamed to test-us01 some time ago (canonical name shared with packaging/common/hosts and the docs); the test scaffold's alias labels and narrative comments hadn't been updated. Pure naming cleanup, no behavior change. The continued dependency on the live external host is tracked separately. --- testing/tor/socks5-outbound/configs/node-a.yaml.tmpl | 4 ++-- testing/tor/socks5-outbound/configs/node-b.yaml.tmpl | 4 ++-- testing/tor/socks5-outbound/docker-compose.yml | 4 ++-- testing/tor/socks5-outbound/scripts/tor-test.sh | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/testing/tor/socks5-outbound/configs/node-a.yaml.tmpl b/testing/tor/socks5-outbound/configs/node-a.yaml.tmpl index 0a48b35..f02511a 100644 --- a/testing/tor/socks5-outbound/configs/node-a.yaml.tmpl +++ b/testing/tor/socks5-outbound/configs/node-a.yaml.tmpl @@ -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" diff --git a/testing/tor/socks5-outbound/configs/node-b.yaml.tmpl b/testing/tor/socks5-outbound/configs/node-b.yaml.tmpl index 640bb0a..77a893c 100644 --- a/testing/tor/socks5-outbound/configs/node-b.yaml.tmpl +++ b/testing/tor/socks5-outbound/configs/node-b.yaml.tmpl @@ -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" diff --git a/testing/tor/socks5-outbound/docker-compose.yml b/testing/tor/socks5-outbound/docker-compose.yml index 7e6bc5c..12d74c5 100644 --- a/testing/tor/socks5-outbound/docker-compose.yml +++ b/testing/tor/socks5-outbound/docker-compose.yml @@ -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: diff --git a/testing/tor/socks5-outbound/scripts/tor-test.sh b/testing/tor/socks5-outbound/scripts/tor-test.sh index 68a21b5..5b26f01 100755 --- a/testing/tor/socks5-outbound/scripts/tor-test.sh +++ b/testing/tor/socks5-outbound/scripts/tor-test.sh @@ -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"