From 14d9bba4ebe3077482a901f366fd2417522e3ff1 Mon Sep 17 00:00:00 2001 From: Jack O'Sullivan Date: Sun, 19 Jul 2026 22:38:02 +0100 Subject: [PATCH] nixos/home: Serve DNS on VRRP VIP and fix ipsec WAN ordering Clients were handed both routers' real addresses as resolvers, so a downed router meant per-query resolver timeouts. Serve pdns-recursor on the VRRP VIPs (with non-local bind so the backup can pre-bind them) and advertise the VIP via kea and radvd, so DNS follows the master. untrusted advertises Cloudflare over v6 to match its v4 config. ipsec started before the WAN's public IP was up: stream's wan carries a static modem address that satisfies wait-online before the DHCP lease, so libreswan loaded its mesh conns (left=) unoriented and never initiated. Gate stream's wan-online.target on the DHCP default route instead, and mkForce ipsec onto wan-online.target only (dropping the strongswan/libreswan multi-user.target pull-in) so the gate actually holds on both boxes. Co-Authored-By: Claude Opus 4.8 --- nixos/boxes/home/routing-common/default.nix | 7 +++++- nixos/boxes/home/routing-common/dns.nix | 11 +++++++++ nixos/boxes/home/routing-common/kea.nix | 6 +++-- nixos/boxes/home/routing-common/radvd.nix | 11 +++++++-- nixos/boxes/home/stream.nix | 26 +++++++++++++++++---- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/nixos/boxes/home/routing-common/default.nix b/nixos/boxes/home/routing-common/default.nix index db228fd..5f17401 100644 --- a/nixos/boxes/home/routing-common/default.nix +++ b/nixos/boxes/home/routing-common/default.nix @@ -161,7 +161,12 @@ in systemd.services = { ipsec = { after = [ "wan-online.target" ]; - wantedBy = [ "wan-online.target" ]; + # strongswan/libreswan force wantedBy=multi-user.target; drop it so the + # target is a true gate rather than mere ordering. This matters most on + # river, where the target is hook-driven and not in the boot transaction, + # so plain ordering wouldn't hold ipsec back at all. partOf re-loads ipsec + # (re-orienting its connections) whenever the WAN drops and returns. + wantedBy = mkForce [ "wan-online.target" ]; partOf = [ "wan-online.target" ]; }; diff --git a/nixos/boxes/home/routing-common/dns.nix b/nixos/boxes/home/routing-common/dns.nix index 1a80c61..04b8368 100644 --- a/nixos/boxes/home/routing-common/dns.nix +++ b/nixos/boxes/home/routing-common/dns.nix @@ -13,6 +13,13 @@ let in { config = { + # Let pdns-recursor bind the VRRP VIPs even on the backup, where the addresses + # aren't present locally + boot.kernel.sysctl = { + "net.ipv4.ip_nonlocal_bind" = 1; + "net.ipv6.ip_nonlocal_bind" = 1; + }; + my = { secrets.files = { "home/pdns/auth.conf" = { @@ -40,6 +47,10 @@ in "127.0.0.1" "::1" assignments.hi.ipv4.address assignments.hi.ipv6.address assignments.lo.ipv4.address assignments.lo.ipv6.address + # VRRP VIPs: DNS follows the master, so clients only ever have one + # (always-live) resolver address and never hang on a dead router + vips.hi.v4 vips.hi.v6 + vips.lo.v4 vips.lo.v6 ]; allow_from = [ "127.0.0.0/8" "::1/128" diff --git a/nixos/boxes/home/routing-common/kea.nix b/nixos/boxes/home/routing-common/kea.nix index 7aa6438..091f42d 100644 --- a/nixos/boxes/home/routing-common/kea.nix +++ b/nixos/boxes/home/routing-common/kea.nix @@ -83,7 +83,8 @@ in } { name = "domain-name-servers"; - data = "${net.cidr.host 1 prefixes.hi.v4}, ${net.cidr.host 2 prefixes.hi.v4}"; + # VRRP VIP so DNS follows the master and clients never hit a dead router + data = vips.hi.v4; } { name = "interface-mtu"; @@ -116,7 +117,8 @@ in } { name = "domain-name-servers"; - data = "${net.cidr.host 1 prefixes.lo.v4}, ${net.cidr.host 2 prefixes.lo.v4}"; + # VRRP VIP so DNS follows the master and clients never hit a dead router + data = vips.lo.v4; } ]; pools = [ diff --git a/nixos/boxes/home/routing-common/radvd.nix b/nixos/boxes/home/routing-common/radvd.nix index fe186b5..1764ed0 100644 --- a/nixos/boxes/home/routing-common/radvd.nix +++ b/nixos/boxes/home/routing-common/radvd.nix @@ -2,7 +2,14 @@ index: { lib, pkgs, ... }: let inherit (lib) mkForce concatMapStringsSep; inherit (lib.my) net; - inherit (lib.my.c.home) domain prefixes; + inherit (lib.my.c.home) domain prefixes vips; + + # untrusted uses external (Cloudflare) resolvers, matching the v4 kea config; + # trusted VLANs use the internal recursor via its floating VRRP VIP + rdnss = name: + if name == "untrusted" + then "2606:4700:4700::1111 2606:4700:4700::1001" + else vips."${name}".v6; mkInterface = name: '' interface lan-${name} { @@ -10,7 +17,7 @@ let AdvRASrcAddress { fe80::1; }; AdvLinkMTU ${toString prefixes."${name}".mtu}; prefix ${prefixes."${name}".v6} {}; - RDNSS ${net.cidr.host 1 prefixes."${name}".v6} ${net.cidr.host 2 prefixes."${name}".v6} {}; + RDNSS ${rdnss name} {}; DNSSL ${domain} dyn.${domain} ${lib.my.c.colony.domain} ${lib.my.c.britway.domain} {}; }; ''; diff --git a/nixos/boxes/home/stream.nix b/nixos/boxes/home/stream.nix index 0ec849b..20f1b7c 100644 --- a/nixos/boxes/home/stream.nix +++ b/nixos/boxes/home/stream.nix @@ -77,11 +77,29 @@ }; }; - # WAN is a plain networkd-managed DHCP link here; gate the shared wan-online - # target on networkd reporting it online. + # wan carries a permanent static modem-management address (assignments.modem) + # alongside the DHCP public IP, so wait-online@wan reports "online" as soon as + # the static address is up - before the DHCP lease arrives. ipsec's left= is the + # public IP, so gating on wait-online lets it start unoriented and never connect. + # Gate instead on the DHCP default route, which only exists once the public lease + # is up (the static modem address has no gateway). + systemd.services.wan-wait-online = { + description = "Wait for the wan default route (public DHCP lease)"; + after = [ "systemd-networkd.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + TimeoutStartSec = "300"; + }; + script = '' + until [ -n "$(${pkgs.iproute2}/bin/ip -4 route show default dev wan)" ]; do + sleep 1 + done + ''; + }; systemd.targets.wan-online = { - requires = [ "systemd-networkd-wait-online@wan.service" ]; - after = [ "systemd-networkd-wait-online@wan.service" ]; + requires = [ "wan-wait-online.service" ]; + after = [ "wan-wait-online.service" ]; wantedBy = [ "multi-user.target" ]; };