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:
@@ -186,6 +186,132 @@ in
|
||||
);
|
||||
};
|
||||
|
||||
wireguard = mkOption {
|
||||
default = { };
|
||||
description = ''
|
||||
WireGuard tunnels, keyed by name. A tunnel is a firewall member like a
|
||||
LAN (it has a `zone`, `internet`, `allowManagement`), so
|
||||
`router.firewall.rules` also cover traffic to and from it. Peers'
|
||||
`allowedIPs` are the destinations routed into the tunnel; routing is
|
||||
by destination only (no default-route tunnels).
|
||||
'';
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
interface = mkOption {
|
||||
type = types.str;
|
||||
default = "wg-${name}";
|
||||
description = "Name of the WireGuard device.";
|
||||
};
|
||||
privateKeyFile = mkOption {
|
||||
type = types.pathWith {
|
||||
absolute = true;
|
||||
inStore = false;
|
||||
};
|
||||
example = "/var/lib/secrets/wg-rw.key";
|
||||
description = ''
|
||||
File with the private key, outside the Nix store. It is
|
||||
handed to systemd-networkd as a systemd credential, so it
|
||||
can be root-only (mode 0600) and is read when networkd starts.
|
||||
'';
|
||||
};
|
||||
listenPort = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "UDP port to listen on (opened on the uplinks); null for a dial-out-only tunnel.";
|
||||
};
|
||||
mtu = mkOption {
|
||||
type = types.nullOr (types.ints.between 576 9000);
|
||||
default = null;
|
||||
description = "Tunnel MTU; null keeps the kernel default (1420).";
|
||||
};
|
||||
addresses = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"10.100.0.1/24"
|
||||
"fd42:100::1/64"
|
||||
];
|
||||
description = ''
|
||||
Addresses (CIDR, IPv4 and/or IPv6) on the tunnel interface;
|
||||
empty means no transfer network (fine for site-to-site).
|
||||
'';
|
||||
};
|
||||
zone = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "Firewall zone; shared with LANs or other tunnels if you want a common policy.";
|
||||
};
|
||||
internet = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether traffic from this tunnel may reach the internet via the uplink.";
|
||||
};
|
||||
allowManagement = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether SSH on the router is reachable through this tunnel.";
|
||||
};
|
||||
dns = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether the router's resolver answers on the tunnel's IPv4 address.";
|
||||
};
|
||||
masquerade = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Source-NAT IPv4 traffic leaving through this tunnel to the tunnel address.";
|
||||
};
|
||||
killSwitch = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Drop forwarded traffic for this tunnel's peers' `allowedIPs`
|
||||
when it would leave through an uplink (for example because
|
||||
the tunnel device is down), instead of letting it leak.
|
||||
'';
|
||||
};
|
||||
peers = mkOption {
|
||||
default = { };
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
publicKey = mkOption { type = types.str; };
|
||||
presharedKeyFile = mkOption {
|
||||
type = types.nullOr (
|
||||
types.pathWith {
|
||||
absolute = true;
|
||||
inStore = false;
|
||||
}
|
||||
);
|
||||
default = null;
|
||||
};
|
||||
endpoint = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "vpn.example.org:51820";
|
||||
};
|
||||
allowedIPs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "192.168.50.0/24" ];
|
||||
};
|
||||
persistentKeepalive = mkOption {
|
||||
type = types.nullOr (types.ints.between 1 65535);
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
firewall.rules = mkOption {
|
||||
default = [ ];
|
||||
description = ''
|
||||
|
||||
Reference in New Issue
Block a user