Add WireGuard tunnels as firewall zone members

router.wireguard.<name> configures tunnels through networkd: road warrior
(listening, dual-stack), site-to-site (no transfer network) and provider
dial-out with masquerade. A tunnel has a zone like a LAN, so inter-zone rules
apply; management, DNS and internet are opt-in. Peers' allowedIPs become
routes (destination-based routing; default-route tunnels are rejected).

Tunnels have a kill switch (default on): forwarded packets for their
allowedIPs are dropped if they would leave via an uplink, before the
established-accept so flows that used the tunnel are cut too.

Private keys stay outside the Nix store and are passed to the unprivileged
systemd-networkd with LoadCredential, so key files can be root-only.

Assertions cover CIDR/key/endpoint formats, duplicate allowedIPs, overlaps
with LAN subnets, port and device clashes, and rule destinations inside
tunnel networks. A five-VM `wireguard` check covers road warrior, site-to-site,
the provider tunnel with an IPv6 transfer net, zone rules and the kill switch.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 19:56:45 +01:00
co-authored by Claude Sonnet 5.5
parent 6015ee2e6d
commit 30ab1a0ed6
11 changed files with 975 additions and 26 deletions
+18 -2
View File
@@ -2,6 +2,21 @@
let
cfg = config.router;
lans = lib.attrValues cfg.lans;
ipv4 = (import ./lib.nix { inherit lib; }).ipv4;
# Tunnels that opted into DNS: listen on each of their addresses.
dnsAddrs = lib.concatMap (w: w.addresses) (lib.filter (w: w.dns) (lib.attrValues cfg.wireguard));
addrOf = a: builtins.head (lib.splitString "/" a);
# IPv4 gets the proper network address; IPv6 netblocks are masked by Unbound.
netOf =
a:
let
parts = lib.splitString "/" a;
in
if lib.hasInfix ":" a then
a
else
ipv4.cidr (builtins.head parts) (lib.toInt (builtins.elemAt parts 1));
in
{
config = lib.mkIf cfg.enable {
@@ -9,13 +24,14 @@ in
enable = true;
settings = {
server = {
interface = [ "127.0.0.1" ] ++ map (l: l.ipv4.address) lans;
interface = [ "127.0.0.1" ] ++ map (l: l.ipv4.address) lans ++ map addrOf dnsAddrs;
# LAN addresses are assigned by networkd, possibly after Unbound starts.
ip-freebind = true;
access-control = [
"127.0.0.0/8 allow"
]
++ map (l: "${l.ipv4.subnet} allow") lans;
++ map (l: "${l.ipv4.subnet} allow") lans
++ map (a: "${netOf a} allow") dnsAddrs;
};
}
// lib.optionalAttrs (cfg.dns.forwarders != [ ]) {