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 ---