- Use set -eu (not set -euf) in sovran-hosts-update.sh - Add sync note for domain validation regex between shell and Python - Rename `seen` to `unique_addresses` in _resolve_all_addresses - Improve loopback override UI message with external DNS check guidance - Add docstring note about first-address display in _resolve_all_addresses"
146 lines
6.4 KiB
Nix
146 lines
6.4 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
|
|
# ── Server-local domain loopback overrides ────────────────────────────────────
|
|
#
|
|
# Some routers (especially newer ISP-provided devices) do not support NAT
|
|
# loopback (hairpin NAT). When a request originates on this computer and
|
|
# targets a public domain name that resolves to the router's WAN address, the
|
|
# router may refuse to loop the connection back in — causing Nextcloud, WordPress
|
|
# background jobs, and other server-side callbacks to fail even when the service
|
|
# is fully operational from the internet.
|
|
#
|
|
# This module installs a one-shot systemd service,
|
|
# ``sovran-hosts-update.service``, that reads the configured service domains
|
|
# from ``/var/lib/domains/`` at boot (and whenever triggered by the Hub after a
|
|
# domain is saved) and writes ``127.0.0.1`` entries for them into a dedicated
|
|
# Sovran-managed block in ``/etc/hosts``.
|
|
#
|
|
# With those entries in place:
|
|
# • Requests originating on this computer resolve the public domain name to
|
|
# 127.0.0.1, reach Caddy directly, and never touch the router.
|
|
# • Caddy still receives the correct public hostname via TLS SNI so virtual-
|
|
# host routing and certificate validation continue to work.
|
|
# • The Sovran Hub can verify Caddy reachability locally without needing NAT
|
|
# loopback.
|
|
#
|
|
# Limitation: this does not help other devices on your home network (phones,
|
|
# laptops). Those devices resolve domains via the router's DNS and still depend
|
|
# on NAT loopback (or require manual router DNS overrides). For now, only
|
|
# server-originated requests benefit from this override.
|
|
#
|
|
# On NixOS, /etc/hosts is normally a symlink into the Nix store and is
|
|
# regenerated by the system activation script. The ``system.activationScripts``
|
|
# hook below converts it to a writable file each time the system is activated
|
|
# (i.e. after every ``nixos-rebuild switch``) and then injects the Sovran block.
|
|
# The same script is also run by the ``sovran-hosts-update.service`` unit so
|
|
# that the Hub can trigger it immediately after saving a domain without
|
|
# requiring a full rebuild.
|
|
|
|
{
|
|
# ── Helper script (stored in the Nix store, never reads /var/lib at eval) ──
|
|
|
|
environment.systemPackages = [ pkgs.coreutils ];
|
|
|
|
environment.etc."sovran-hosts-update.sh" = {
|
|
mode = "0755";
|
|
text = ''
|
|
#!/bin/sh
|
|
# Regenerate the Sovran-managed loopback block in /etc/hosts.
|
|
# Safe to run multiple times — idempotent.
|
|
set -eu
|
|
|
|
DOMAINS_DIR="/var/lib/domains"
|
|
HOSTS_FILE="/etc/hosts"
|
|
BEGIN_MARKER="# Sovran managed begin — server-local loopback overrides"
|
|
END_MARKER="# Sovran managed end"
|
|
|
|
# ── Step 1: ensure /etc/hosts is a regular writable file ──────────────
|
|
# On NixOS /etc/hosts starts as a symlink to the Nix store. We replace
|
|
# it with a copy so we can append our block without touching the store.
|
|
if [ -L "$HOSTS_FILE" ]; then
|
|
TARGET=$(readlink -f "$HOSTS_FILE")
|
|
cp --no-preserve=all "$TARGET" "$HOSTS_FILE.sovran-tmp"
|
|
mv "$HOSTS_FILE.sovran-tmp" "$HOSTS_FILE"
|
|
chmod 644 "$HOSTS_FILE"
|
|
fi
|
|
|
|
# ── Step 2: remove any existing Sovran block ──────────────────────────
|
|
# Use a temp file so the operation is atomic.
|
|
TMP=$(mktemp "$HOSTS_FILE.XXXXXX")
|
|
trap 'rm -f "$TMP"' EXIT
|
|
awk "
|
|
/^$BEGIN_MARKER\$/ { skip=1; next }
|
|
/^$END_MARKER\$/ { skip=0; next }
|
|
!skip
|
|
" "$HOSTS_FILE" > "$TMP"
|
|
|
|
# ── Step 3: collect valid configured service domains ──────────────────
|
|
# NOTE: The hostname validation regex below must stay in sync with
|
|
# _SAFE_DOMAIN_RE in app/sovran_systemsos_web/server.py.
|
|
ENTRIES=""
|
|
for KEY in matrix wordpress nextcloud btcpayserver vaultwarden haven element-calling; do
|
|
FILE="$DOMAINS_DIR/$KEY"
|
|
[ -f "$FILE" ] || continue
|
|
# Read the domain value (strip all whitespace, limit to 253 chars)
|
|
DOMAIN=$(tr -d '[:space:]' < "$FILE" | head -c 253)
|
|
[ -z "$DOMAIN" ] && continue
|
|
# Validate: must match a reasonable hostname pattern (no injection)
|
|
if ! printf '%s' "$DOMAIN" | grep -qE \
|
|
'^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$'; then
|
|
echo "sovran-hosts-update: skipping invalid domain value for $KEY: $DOMAIN" >&2
|
|
continue
|
|
fi
|
|
ENTRIES="$ENTRIES
|
|
127.0.0.1 $DOMAIN
|
|
::1 $DOMAIN"
|
|
done
|
|
|
|
# ── Step 4: append the Sovran block if there are any entries ──────────
|
|
if [ -n "$ENTRIES" ]; then
|
|
printf '\n%s\n' "$BEGIN_MARKER" >> "$TMP"
|
|
printf '%s\n' "# These entries route configured service domains to local Caddy." >> "$TMP"
|
|
printf '%s\n' "# They are managed automatically — do not edit this block." >> "$TMP"
|
|
printf '%s\n' "$ENTRIES" >> "$TMP"
|
|
printf '%s\n' "$END_MARKER" >> "$TMP"
|
|
fi
|
|
|
|
# ── Step 5: atomically replace /etc/hosts ─────────────────────────────
|
|
mv "$TMP" "$HOSTS_FILE"
|
|
chmod 644 "$HOSTS_FILE"
|
|
'';
|
|
};
|
|
|
|
# ── Systemd service ────────────────────────────────────────────────────────
|
|
|
|
systemd.services.sovran-hosts-update = {
|
|
description = "Update /etc/hosts with Sovran server-local loopback overrides";
|
|
documentation = [ "https://github.com/naturallaw777/sovran-systems" ];
|
|
|
|
# Run before Caddy so loopback entries are ready when it starts.
|
|
before = [
|
|
"caddy.service"
|
|
"network-online.target"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
ExecStart = "/etc/sovran-hosts-update.sh";
|
|
};
|
|
};
|
|
|
|
# ── Activation script (runs after every nixos-rebuild switch) ─────────────
|
|
# This ensures the loopback block survives rebuilds that restore the /etc/hosts
|
|
# symlink. The "users" and "etc" scripts must complete first.
|
|
|
|
system.activationScripts.sovranDomainLoopback = {
|
|
text = ''
|
|
if [ -x /etc/sovran-hosts-update.sh ] && [ -d /var/lib/domains ]; then
|
|
/etc/sovran-hosts-update.sh || true
|
|
fi
|
|
'';
|
|
deps = [ "etc" "users" ];
|
|
};
|
|
}
|