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:
+61
-11
@@ -4,19 +4,59 @@ let
|
||||
lans = lib.attrValues cfg.lans;
|
||||
|
||||
set = ifaces: "{ ${lib.concatMapStringsSep ", " (i: ''"${i}"'') ifaces} }";
|
||||
lanIfaces = map (l: l.device) lans;
|
||||
mgmtIfaces = map (l: l.device) (lib.filter (l: l.allowManagement) lans);
|
||||
internetIfaces = map (l: l.device) (lib.filter (l: l.internet) lans);
|
||||
wgs = lib.attrValues cfg.wireguard;
|
||||
isV6 = lib.hasInfix ":";
|
||||
|
||||
# zone name -> devices of its member LANs
|
||||
zones = lib.mapAttrs (_: ls: map (l: l.device) ls) (lib.groupBy (l: l.zone) lans);
|
||||
# Everything that is a firewall zone member: LANs and tunnels.
|
||||
members =
|
||||
map (l: {
|
||||
inherit (l)
|
||||
device
|
||||
zone
|
||||
internet
|
||||
allowManagement
|
||||
;
|
||||
}) lans
|
||||
++ map (w: {
|
||||
device = w.interface;
|
||||
inherit (w) zone internet allowManagement;
|
||||
}) wgs;
|
||||
devicesOf = pred: map (m: m.device) (lib.filter pred members);
|
||||
|
||||
lanIfaces = map (l: l.device) lans;
|
||||
memberIfaces = devicesOf (_: true);
|
||||
dnsIfaces = lanIfaces ++ map (w: w.interface) (lib.filter (w: w.dns) wgs);
|
||||
mgmtIfaces = devicesOf (m: m.allowManagement);
|
||||
internetIfaces = devicesOf (m: m.internet);
|
||||
listenPorts = lib.unique (map (w: w.listenPort) (lib.filter (w: w.listenPort != null) wgs));
|
||||
masqueradeIfaces = map (w: w.interface) (lib.filter (w: w.masquerade) wgs);
|
||||
|
||||
# Destinations a kill-switched tunnel must never leak to an uplink.
|
||||
killSwitched = lib.filter (w: w.killSwitch) wgs;
|
||||
killNets = lib.concatMap (w: lib.concatMap (p: p.allowedIPs) (lib.attrValues w.peers)) killSwitched;
|
||||
killRules =
|
||||
let
|
||||
v4 = lib.filter (n: !isV6 n) killNets;
|
||||
v6 = lib.filter isV6 killNets;
|
||||
list = ns: "{ ${lib.concatStringsSep ", " ns} }";
|
||||
in
|
||||
lib.optional (v4 != [ ]) "oifname ${set uplinks} ip daddr ${list v4} drop"
|
||||
++ lib.optional (v6 != [ ]) "oifname ${set uplinks} ip6 daddr ${list v6} drop";
|
||||
|
||||
# zone name -> devices of its members
|
||||
zones = lib.mapAttrs (_: ms: map (m: m.device) ms) (lib.groupBy (m: m.zone) members);
|
||||
zoneDevices = zone: zones.${zone} or [ ];
|
||||
|
||||
# LANs sharing a zone may forward to each other (a single LAN is plain L2).
|
||||
# Members of one zone may forward to each other, but never back out of the
|
||||
# device they came in on (a tunnel's peers do not see each other).
|
||||
intraZone = lib.concatLists (
|
||||
lib.mapAttrsToList (
|
||||
zone: devs:
|
||||
lib.optional (lib.length devs > 1) "iifname ${set devs} oifname ${set devs} accept # zone ${zone}"
|
||||
lib.optionals (lib.length devs > 1) (
|
||||
map (
|
||||
d: "iifname ${set [ d ]} oifname ${set (lib.filter (x: x != d) devs)} accept # zone ${zone}"
|
||||
) devs
|
||||
)
|
||||
) zones
|
||||
);
|
||||
|
||||
@@ -69,14 +109,19 @@ in
|
||||
destination-unreachable, packet-too-big, time-exceeded, parameter-problem,
|
||||
nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert
|
||||
} accept
|
||||
iifname ${set lanIfaces} icmp type echo-request accept
|
||||
iifname ${set lanIfaces} icmpv6 type echo-request accept
|
||||
iifname ${set memberIfaces} icmp type echo-request accept
|
||||
iifname ${set memberIfaces} icmpv6 type echo-request accept
|
||||
|
||||
${lib.optionalString (!pppoe) ''iifname "${cfg.wan.interface}" udp dport 68 accept''}
|
||||
${lib.optionalString cfg.wan.ipv6.enable ''iifname "${cfg.wan.interface}" udp dport 546 accept''}
|
||||
|
||||
iifname ${set lanIfaces} udp dport 53 accept
|
||||
iifname ${set lanIfaces} tcp dport 53 accept
|
||||
iifname ${set dnsIfaces} udp dport 53 accept
|
||||
iifname ${set dnsIfaces} tcp dport 53 accept
|
||||
${lib.optionalString (listenPorts != [ ])
|
||||
"iifname ${set uplinks} udp dport { ${
|
||||
lib.concatMapStringsSep ", " toString listenPorts
|
||||
} } accept # wireguard"
|
||||
}
|
||||
${lib.optionalString (dhcpIfaces != [ ]) "iifname ${set dhcpIfaces} udp dport 67 accept"}
|
||||
${lib.optionalString (mgmtIfaces != [ ]) "iifname ${set mgmtIfaces} tcp dport 22 accept"}
|
||||
}
|
||||
@@ -84,6 +129,8 @@ in
|
||||
chain forward {
|
||||
type filter hook forward priority filter; policy drop;
|
||||
|
||||
${lib.concatStringsSep "\n " killRules}
|
||||
|
||||
ct state established,related accept
|
||||
ct state invalid drop
|
||||
|
||||
@@ -110,6 +157,9 @@ in
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
oifname "${uplink4}" masquerade
|
||||
${lib.optionalString (
|
||||
masqueradeIfaces != [ ]
|
||||
) "oifname ${set masqueradeIfaces} masquerade # tunnels"}
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user