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:
+162
-1
@@ -49,6 +49,18 @@ let
|
||||
} overrides
|
||||
);
|
||||
|
||||
# A throwaway public key, valid in format only.
|
||||
pub = "7MkFXOxlCsmXm98uv0txvYzx5Ce80mkg2mDsswY4FyI=";
|
||||
wg =
|
||||
overrides:
|
||||
lib.recursiveUpdate {
|
||||
privateKeyFile = "/var/lib/secrets/wg.key";
|
||||
peers.p1 = {
|
||||
publicKey = pub;
|
||||
allowedIPs = [ "10.100.0.2/32" ];
|
||||
};
|
||||
} overrides;
|
||||
|
||||
failed = router: map (a: a.message) (lib.filter (a: !a.assertion) (evalRouter router).assertions);
|
||||
|
||||
cases = {
|
||||
@@ -328,7 +340,7 @@ let
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "is not inside any LAN of zone `main`" ];
|
||||
expect = [ "is not inside any network of zone `main`" ];
|
||||
};
|
||||
ruleDestinationHostBits = {
|
||||
router = {
|
||||
@@ -346,6 +358,154 @@ let
|
||||
};
|
||||
expect = [ "has host bits set" ];
|
||||
};
|
||||
wgValid = {
|
||||
router = {
|
||||
wan.interface = "eth0";
|
||||
lans.main = lan { };
|
||||
wireguard = {
|
||||
rw = wg {
|
||||
listenPort = 51820;
|
||||
addresses = [
|
||||
"10.100.0.1/24"
|
||||
"fd42:100::1/64"
|
||||
];
|
||||
dns = true;
|
||||
peers.p1.allowedIPs = [
|
||||
"10.100.0.2/32"
|
||||
"fd42:100::2/128"
|
||||
];
|
||||
};
|
||||
s2s = wg {
|
||||
listenPort = 51821;
|
||||
zone = "remote";
|
||||
peers.p1 = {
|
||||
endpoint = "203.0.113.60:51821";
|
||||
allowedIPs = [ "192.168.50.0/24" ];
|
||||
persistentKeepalive = 25;
|
||||
};
|
||||
};
|
||||
ff = wg {
|
||||
zone = "freifunk";
|
||||
addresses = [
|
||||
"10.200.0.2/32"
|
||||
"fd42:200::2/128"
|
||||
];
|
||||
masquerade = true;
|
||||
peers.p1 = {
|
||||
endpoint = "[2001:db8::1]:51822";
|
||||
allowedIPs = [
|
||||
"198.18.0.0/24"
|
||||
"fd00:ff::/32"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "main";
|
||||
to = "remote";
|
||||
destination = "192.168.50.7";
|
||||
}
|
||||
{
|
||||
from = "main";
|
||||
to = "freifunk";
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ ];
|
||||
};
|
||||
wgNoPeers = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers = lib.mkForce { }; };
|
||||
};
|
||||
expect = [ "has no peers" ];
|
||||
};
|
||||
wgBadAddress = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { addresses = [ "10.100.0.1" ]; };
|
||||
};
|
||||
expect = [ "addresses must be IPv4/IPv6 addresses in CIDR notation" ];
|
||||
};
|
||||
wgDefaultRoute = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.allowedIPs = [ "0.0.0.0/0" ]; };
|
||||
};
|
||||
expect = [ "default-route tunnels" ];
|
||||
};
|
||||
wgDuplicateAllowedIps = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg {
|
||||
peers.p2 = {
|
||||
publicKey = pub;
|
||||
allowedIPs = [ "10.100.0.2/32" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
expect = [ "appears more than once" ];
|
||||
};
|
||||
wgOverlapsLan = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.allowedIPs = [ "192.168.10.0/25" ]; };
|
||||
};
|
||||
expect = [ "overlaps one of the LAN subnets" ];
|
||||
};
|
||||
wgBadKey = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.publicKey = "not-a-key"; };
|
||||
};
|
||||
expect = [ "not a valid WireGuard public key" ];
|
||||
};
|
||||
wgBadEndpoint = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { peers.p1.endpoint = "vpn.example.org"; };
|
||||
};
|
||||
expect = [ "endpoint must look like host:port" ];
|
||||
};
|
||||
wgSamePort = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard = {
|
||||
a = wg { listenPort = 51820; };
|
||||
b = wg { listenPort = 51820; };
|
||||
};
|
||||
};
|
||||
expect = [ "listen on the same UDP port" ];
|
||||
};
|
||||
wgDeviceClash = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.rw = wg { interface = "eth1"; };
|
||||
};
|
||||
expect = [ "clashes with a LAN device or the WAN interface" ];
|
||||
};
|
||||
wgNameTooLong = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.a-very-long-tunnel-name = wg { };
|
||||
};
|
||||
expect = [ "longer than 15 characters" ];
|
||||
};
|
||||
wgRuleDestinationOutside = {
|
||||
router = {
|
||||
lans.main = lan { };
|
||||
wireguard.s2s = wg { peers.p1.allowedIPs = [ "192.168.50.0/24" ]; };
|
||||
firewall.rules = [
|
||||
{
|
||||
from = "main";
|
||||
to = "s2s";
|
||||
destination = "192.168.60.1";
|
||||
}
|
||||
];
|
||||
};
|
||||
expect = [ "is not inside any network of zone `s2s`" ];
|
||||
};
|
||||
nonOverlappingLans = {
|
||||
router.lans = {
|
||||
a = lan { };
|
||||
@@ -391,6 +551,7 @@ let
|
||||
"pppoeValid"
|
||||
"vlansOnOneTrunk"
|
||||
"ruleValid"
|
||||
"wgValid"
|
||||
];
|
||||
|
||||
subnet =
|
||||
|
||||
Reference in New Issue
Block a user