{ config, lib, ... }: let inherit (lib) mkEnableOption mkOption types mkIf attrValues replaceString ; cfg = config.foundation.vpn; inherit (import ../../assets/vpn.nix) users; interface = "vpn0"; port = 51820; # while the tunnel uses the lower subnets for the paths, # the vpn always uses subnet number 10. subnetIndex = 10; addressFromTemplate = template: prefix: "${replaceString "X" (toString subnetIndex) template}/${toString prefix}"; in { options.foundation.vpn = { enable = mkEnableOption "WireGuard VPN server"; externalInterface = mkOption { type = types.str; description = "External network interface"; }; }; config = mkIf cfg.enable { age.secrets.wg-private-key = { file = ../../secrets/wg-private-key.age; owner = "systemd-network"; }; networking.firewall.allowedUDPPorts = [ port ]; systemd.network = { netdevs."30-${interface}" = { netdevConfig = { Kind = "wireguard"; Name = interface; }; wireguardConfig = { PrivateKeyFile = config.age.secrets.wg-private-key.path; ListenPort = port; }; wireguardPeers = map (user: { PublicKey = user.key; AllowedIPs = [ (addressFromTemplate user.ip 32) ]; }) (attrValues users); }; networks."30-${interface}" = { name = interface; address = [ (addressFromTemplate "10.123.X.1" 24) ]; linkConfig = { RequiredForOnline = "no"; }; }; }; networking.nftables.tables.vpn-nat = { family = "ip"; content = '' chain postrouting { type nat hook postrouting priority srcnat; policy accept; iifname "${interface}" oifname "${cfg.externalInterface}" masquerade } chain forward { type filter hook forward priority 0; policy accept; iifname "${interface}" oifname "${cfg.externalInterface}" accept iifname "${cfg.externalInterface}" oifname "${interface}" ct state established,related accept # drop vpn-sourced traffic headed anywhere but external iifname "${interface}" drop } ''; }; }; }