Add local DNS records, DHCP reservations and an installer ISO

DNS and DHCP (needed to replace a dnsmasq-based router):
- router.dns.domain/hosts/overrides/forwardZones: authoritative local
  domain, split-horizon host records with reverse entries, wildcard
  overrides and per-zone forwarders (private zones skip DNSSEC).
- lans.<n>.dhcp.reservations: Kea host reservations, published in DNS.
- Tunnels with dns = true admit what their peers route, so a
  site-to-site peer without a transfer address can use the resolver.

Installer:
- router-install wipes a disk, creates an ESP and an ext4 root and
  installs a prebuilt system with its secrets; nothing is built on the
  box. installed-layout is the shared disk layout contract.
- lib.mkInstaller builds an ISO for a given nixosSystem;
  packages.installer-example is the example's ISO.

Covered by the dns and installer VM tests (the latter installs onto an
empty disk and boots it under UEFI), wireguard DNS and assertion cases.
PLAN.md records the remaining gaps for replacing the hand-written router.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 12:31:44 +01:00
co-authored by Claude Sonnet 5.5
parent 0653b5cf2f
commit b5429ad876
15 changed files with 943 additions and 21 deletions
+118
View File
@@ -0,0 +1,118 @@
# `router-install`: write a prebuilt router system to a disk. Nothing is built
# on the box; the system closure travels with the installer.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.router.installer;
installScript = pkgs.writeShellApplication {
name = "router-install";
runtimeInputs = [
config.system.build.nixos-install
pkgs.coreutils
pkgs.dosfstools
pkgs.e2fsprogs
pkgs.gptfdisk
pkgs.util-linux
];
text = ''
usage() {
echo "usage: router-install [--yes] [--secrets DIR] <disk>" >&2
echo " Wipes <disk> (e.g. /dev/nvme0n1) and installs ${cfg.system}." >&2
echo " --secrets DIR copied to /var/lib/router (PPPoE password, WireGuard keys, ...)" >&2
exit 2
}
yes=0
secrets=
disk=
while [ $# -gt 0 ]; do
case $1 in
--yes) yes=1 ;;
--secrets) shift; [ $# -gt 0 ] || usage; secrets=$1 ;;
-*) usage ;;
*) [ -z "$disk" ] || usage; disk=$1 ;;
esac
shift
done
[ -n "$disk" ] || usage
[ "$(id -u)" -eq 0 ] || { echo "router-install: must run as root" >&2; exit 1; }
[ -b "$disk" ] || { echo "router-install: $disk is not a block device" >&2; exit 1; }
if [ -n "$secrets" ] && [ ! -d "$secrets" ]; then
echo "router-install: $secrets is not a directory" >&2
exit 1
fi
if lsblk -nr -o MOUNTPOINTS "$disk" | grep -q .; then
echo "router-install: $disk or one of its partitions is mounted; refusing" >&2
exit 1
fi
[ -d /sys/firmware/efi ] || echo "warning: not booted in UEFI mode; the box itself must boot UEFI" >&2
echo "About to ERASE $disk:"
lsblk -o NAME,SIZE,MODEL,FSTYPE,LABEL "$disk"
if [ "$yes" -ne 1 ]; then
printf 'Type the device name (%s) to continue: ' "$disk"
read -r answer
[ "$answer" = "$disk" ] || { echo "aborted" >&2; exit 1; }
fi
part() {
case $disk in
*[0-9]) echo "''${disk}p$1" ;;
*) echo "$disk$1" ;;
esac
}
echo ">> partitioning"
sgdisk --zap-all "$disk" >/dev/null
sgdisk -n 1:0:+512M -t 1:ef00 -c 1:boot -n 2:0:0 -t 2:8300 -c 2:nixos "$disk" >/dev/null
partprobe "$disk" || true
udevadm settle
echo ">> formatting"
mkfs.fat -F 32 -n boot "$(part 1)" >/dev/null
mkfs.ext4 -q -F -L nixos "$(part 2)"
echo ">> mounting"
mkdir -p /mnt
mount "$(part 2)" /mnt
mkdir -p /mnt/boot
mount -o umask=0077 "$(part 1)" /mnt/boot
if [ -n "$secrets" ]; then
echo ">> copying secrets"
install -d -m 0700 /mnt/var/lib/router
cp -rT "$secrets" /mnt/var/lib/router
chown -R root:root /mnt/var/lib/router
chmod -R go-rwx /mnt/var/lib/router
else
echo "note: no --secrets given; put the PPPoE password etc. in /var/lib/router later" >&2
fi
echo ">> installing ${cfg.system}"
nixos-install --system ${cfg.system} --root /mnt --no-root-passwd --no-channel-copy
echo ">> unmounting"
umount -R /mnt
sync
echo "Done. Remove the installer and reboot."
'';
};
in
{
options.router.installer.system = lib.mkOption {
type = lib.types.package;
description = "The system (a `system.build.toplevel`) `router-install` writes to disk.";
};
config = {
environment.systemPackages = [ installScript ];
# The closure must be on the installer itself.
system.extraDependencies = [ cfg.system ];
};
}