Files
Johnathan Corgan 724b9145a1 Correct DNS, MTU and FreeBSD TUN text, and remove fips-dns-setup's unreachable resolvectl backend
The header of fips-dns-setup said .fips queries are forwarded to
127.0.0.1:5354, with [::1]:5354 only for the global drop-in. Every
backend but the per-link resolvectl fallback targets [::1]:5354, the
daemon's default bind; the fallback targets the TUN's own address.

The configuration reference said dns.ttl should not exceed
node.cache.coord_ttl_secs to avoid stale address mappings. The two are
unrelated: an <npub>.fips answer is derived from the npub and cannot go
stale. The reference now names the case that can, a host alias pointed
at another npub, which a client may keep for up to dns.ttl seconds.

The host aliases guide said hostnames are lowercase. Either case is
accepted, and names are stored and matched in lowercase.

The two Nostr relay examples advised FIPS_UDP_MTU=1280 for deployments
safe at the IPv6 minimum. A 1280-byte UDP transport is the one that
cannot carry a 1280-byte IPv6 packet: its effective IPv6 MTU is 1203,
IPv6 packets of 1204 to 1280 bytes other than TCP are lost, and the node
warns about it at start-up. The examples now name 1357, the smallest
value that carries every IPv6 packet up to 1280 bytes.

The FreeBSD package README and two code comments said the kernel
destroys the tun interface when the daemon exits. The daemon only takes
it down and never destroys it, and the next start reopens it. The README
and comments now say so, and attribute what happens to the interface in
between to what was observed on pfSense: it stays listed, down and
without an address. The delete_interface comment no longer claims that
taking the interface down wakes a blocked reader.

The per-link resolvectl backend ran only when systemd-resolved was
active. That is also the only condition the global drop-in backend
checks, and the drop-in backend runs first and returns success once the
check passes. So the resolvectl backend was reached only if resolved
became active between those two checks, and it then pointed resolved at
the TUN's mesh address, where the daemon's default ::1 bind does not
listen.

Remove it, with the TUN name lookup and the interface waits that only it
used, and the hermetic test of that lookup in both CI runners. Every
remaining backend targets [::1]:5354, and none uses the TUN interface,
so fips-dns-setup does not wait for one, on a host with any tun.name.
fips-dns-teardown still reverts the per-link configuration an older
version could write.

The systemd install README listed the backend, and said the responder
listens on fips0 as well as [::1]:5354; by default it listens on
[::1]:5354 only. Three comments in the Debian install test still
described the wait for fips0.
2026-10-27 21:51:02 +00:00

224 lines
9.0 KiB
Bash
Executable File

#!/bin/bash
# Single-container entrypoint: generate the FIPS config, apply iptables
# isolation, start the Nostr relay (strfry + nginx), start the Nym SOCKS5
# client (only when the FIPS config enables the nym transport), and launch
# FIPS last — so the mixnet proxy is provably up before FIPS dials its peer.
set -e
# --- Generate FIPS config from environment variables ---
FIPS_NSEC="${FIPS_NSEC:?FIPS_NSEC is required}"
FIPS_UDP_BIND="${FIPS_UDP_BIND:-0.0.0.0:2121}"
FIPS_TCP_BIND="${FIPS_TCP_BIND:-0.0.0.0:8443}"
FIPS_TUN_MTU="${FIPS_TUN_MTU:-1280}"
FIPS_UDP_MTU="${FIPS_UDP_MTU:-1472}"
FIPS_PEER_TRANSPORT="${FIPS_PEER_TRANSPORT:-nym}"
FIPS_NYM_SOCKS5_ADDR="${FIPS_NYM_SOCKS5_ADDR:-127.0.0.1:1080}"
NYM_CLIENT_ID="${NYM_CLIENT_ID:-fips-nym-client}"
NYM_STARTUP_TIMEOUT="${NYM_STARTUP_TIMEOUT:-180}"
mkdir -p /etc/fips
# Build peers section
PEERS_SECTION=""
if [ -n "$FIPS_PEER_NPUB" ] && [ -n "$FIPS_PEER_ADDR" ]; then
FIPS_PEER_ALIAS="${FIPS_PEER_ALIAS:-peer}"
PEERS_SECTION=" - npub: \"${FIPS_PEER_NPUB}\"
alias: \"${FIPS_PEER_ALIAS}\"
addresses:
- transport: ${FIPS_PEER_TRANSPORT}
addr: \"${FIPS_PEER_ADDR}\"
connect_policy: auto_connect"
fi
# The nym transport block is emitted only in nym mode; the SOCKS5 client
# below starts only when this block is present in the config.
NYM_SECTION=""
if [ "$FIPS_PEER_TRANSPORT" = "nym" ]; then
NYM_SECTION=" nym:
socks5_addr: \"${FIPS_NYM_SOCKS5_ADDR}\"
startup_timeout_secs: 120"
fi
cat > /etc/fips/fips.yaml <<EOF
node:
identity:
nsec: "${FIPS_NSEC}"
tun:
enabled: true
name: fips0
mtu: ${FIPS_TUN_MTU}
dns:
enabled: true
bind_addr: "127.0.0.1"
transports:
udp:
bind_addr: "${FIPS_UDP_BIND}"
# 1472 = Docker bridge IPv4 max (1500 MTU - 8 UDP - 20 IPv4 header).
# Keep FIPS_UDP_MTU at 1357 or more, the smallest that carries every
# IPv6 packet up to 1280 bytes; lower it only for an underlay path that
# cannot carry 1385-byte IPv4 packets.
mtu: ${FIPS_UDP_MTU}
tcp:
bind_addr: "${FIPS_TCP_BIND}"
${NYM_SECTION}
peers:
${PEERS_SECTION:- []}
EOF
echo "Generated /etc/fips/fips.yaml"
# --- Start local DNS first ---
# resolv.conf points at 127.0.0.1; dnsmasq must be up before anything
# below (peer-IP resolution, harbourmaster query, nym gateway lookup).
dnsmasq
# --- Apply iptables rules for strict network isolation ---
#
# Goal: only FIPS transport traffic may use eth0. All other eth0 traffic is
# dropped. fips0 and loopback are unrestricted, so the relay (sharing this
# namespace) is reachable only over the FIPS mesh.
#
# In nym mode the direct route to the peer is explicitly DROPped before the
# general TCP accept for the mixnet gateways: if the peer comes up, the
# connection can only have travelled through the mixnet.
# IPv4: allow only FIPS transport on eth0
iptables -A OUTPUT -o lo -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --dport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p udp --sport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT
iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --sport 443 -j ACCEPT
PEER_HOST="${FIPS_PEER_ADDR%:*}"
PEER_PORT="${FIPS_PEER_ADDR##*:}"
case "$FIPS_PEER_TRANSPORT" in
nym)
# Block the direct path to the peer (mixnet-only proof). IPv4 only:
# eth0 IPv6 is dropped wholesale by the ip6tables rules below.
if [ -n "$FIPS_PEER_ADDR" ]; then
PEER_IP=$(getent ahostsv4 "$PEER_HOST" | awk '{print $1; exit}' || true)
if [ -n "$PEER_IP" ]; then
iptables -A OUTPUT -o eth0 -p tcp -d "$PEER_IP" --dport "$PEER_PORT" -j DROP
echo "Direct path to peer ${PEER_HOST} (${PEER_IP}:${PEER_PORT}) blocked — mixnet only"
fi
fi
# … then allow outbound TCP for the nym client's gateway connections.
iptables -A OUTPUT -o eth0 -p tcp -j ACCEPT
iptables -A INPUT -i eth0 -p tcp -m state --state ESTABLISHED,RELATED -j ACCEPT
;;
tcp)
# Allow dialing the peer's TCP endpoint directly, and inbound FIPS TCP.
if [ -n "$FIPS_PEER_ADDR" ]; then
iptables -A OUTPUT -o eth0 -p tcp --dport "$PEER_PORT" -j ACCEPT
iptables -A INPUT -i eth0 -p tcp --sport "$PEER_PORT" -m state --state ESTABLISHED,RELATED -j ACCEPT
fi
iptables -A INPUT -i eth0 -p tcp --dport "${FIPS_TCP_BIND##*:}" -j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp --sport "${FIPS_TCP_BIND##*:}" -j ACCEPT
;;
esac
iptables -A OUTPUT -o eth0 -j DROP
iptables -A INPUT -i eth0 -j DROP
# IPv6: allow fips0 and loopback, block eth0
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o fips0 -j ACCEPT
ip6tables -A INPUT -i fips0 -j ACCEPT
ip6tables -A OUTPUT -o eth0 -j DROP
ip6tables -A INPUT -i eth0 -j DROP
echo "iptables isolation rules applied"
# --- Start the Nostr relay app (strfry + nginx) ---
(cd /usr/src/app && exec strfry relay) &
nginx
echo "Nostr relay started (strfry on 127.0.0.1:7777, nginx on :80)"
# --- Start the Nym SOCKS5 client (only if the config enables nym) ---
if [ "$FIPS_PEER_TRANSPORT" = "nym" ]; then
NYM_HOST="${FIPS_NYM_SOCKS5_ADDR%:*}"
NYM_PORT="${FIPS_NYM_SOCKS5_ADDR##*:}"
# Auto-discover a network-requester service provider when none is set.
if [ ! -d "${HOME}/.nym/socks5-clients/${NYM_CLIENT_ID}" ]; then
# The provider is only consulted at init time, so auto-discovery
# runs only when a fresh client must be initialized. Failures
# (harbourmaster down or serving HTML) must not crash the
# container with a bare jq error — hence the `|| true` guards
# and the explicit empty-check below.
if [ -z "$NYM_SERVICE_PROVIDER" ]; then
echo "NYM_SERVICE_PROVIDER not set — querying harbourmaster.nymtech.net …"
NYM_SERVICE_PROVIDER=$(curl -fsSL --retry 3 \
"https://harbourmaster.nymtech.net/v2/services?order_by=routing_score&order_direction=desc&size=100" \
2>/dev/null \
| jq -r '[.items[] | select(.routing_score == 1.0)]
| sort_by(.last_updated_utc) | last
| .service_provider_client_id // empty' 2>/dev/null \
|| true)
if [ -z "$NYM_SERVICE_PROVIDER" ]; then
# Last resort: a provider known to work at the time of
# writing (2026-06). Providers are volatile community
# infra — if the mixnet connects but no traffic flows
# ('no node with identity … is known' warnings), this
# fallback has gone stale: pick a current one from
# https://harbourmaster.nymtech.net/ and set it in .env.
NYM_SERVICE_PROVIDER="${NYM_FALLBACK_PROVIDER:-7sfw3sEtSPwhWLmEasVmPXKxqioCo4GaXRkm9bW6yWGZ.CkhMoH85wfNcV2fwoBjc6QDbcaFZHzKqFFvXWfYMw19y@4ScsM6AVowhKTMWaH98NLntKDwbu2ZMEycUk4mZiZppG}"
echo "WARNING: harbourmaster auto-discovery failed — using the" >&2
echo "baked-in fallback provider (may be stale; see .env):" >&2
echo " ${NYM_SERVICE_PROVIDER}" >&2
else
echo "Auto-selected service provider: ${NYM_SERVICE_PROVIDER}"
fi
fi
echo "Initializing Nym SOCKS5 client '${NYM_CLIENT_ID}' …"
nym-socks5-client init \
--id "${NYM_CLIENT_ID}" \
--provider "${NYM_SERVICE_PROVIDER}" \
--port "${NYM_PORT}" \
--host "${NYM_HOST}"
else
# The provider is baked into the client state at init time — a value
# set or discovered now does NOT apply to an existing client. Surface
# the one actually in effect so a stale/dead provider isn't chased
# silently (symptom: 'no node with identity … is known' warnings).
STORED_PROVIDER=$(grep -m1 -oE '[1-9A-HJ-NP-Za-km-z]{20,}\.[1-9A-HJ-NP-Za-km-z]{20,}@[1-9A-HJ-NP-Za-km-z]{20,}' \
"${HOME}/.nym/socks5-clients/${NYM_CLIENT_ID}/config/config.toml" 2>/dev/null || true)
echo "Reusing existing Nym client state (provider: ${STORED_PROVIDER:-unknown})."
echo "To switch provider, remove the nym-data volume: docker compose down -v"
fi
echo "Starting Nym SOCKS5 client (mixnet bootstrap may take a minute) …"
nym-socks5-client run \
--id "${NYM_CLIENT_ID}" \
--port "${NYM_PORT}" \
--host "${NYM_HOST}" &
# FIPS must not start dialing before the proxy accepts connections.
elapsed=0
until nc -z "$NYM_HOST" "$NYM_PORT" 2>/dev/null; do
if [ "$elapsed" -ge "$NYM_STARTUP_TIMEOUT" ]; then
echo "ERROR: Nym SOCKS5 proxy not ready after ${NYM_STARTUP_TIMEOUT}s" >&2
exit 1
fi
sleep 2
elapsed=$((elapsed + 2))
done
echo "Nym SOCKS5 proxy ready at ${FIPS_NYM_SOCKS5_ADDR} (after ~${elapsed}s)"
fi
# --- Launch FIPS (container lifecycle follows the daemon) ---
echo "Starting FIPS daemon..."
exec fips --config /etc/fips/fips.yaml