diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix index 2130c49f374e..466c6d3cbfd6 100644 --- a/nixos/modules/services/networking/wg-quick.nix +++ b/nixos/modules/services/networking/wg-quick.nix @@ -11,6 +11,15 @@ let interfaceOpts = { ... }: { options = { + type = mkOption { + example = "amneziawg"; + default = "wireguard"; + type = types.enum ["wireguard" "amneziawg"]; + description = '' + The type of the interface. Currently only "wireguard" and "amneziawg" are supported. + ''; + }; + configFile = mkOption { example = "/secret/wg0.conf"; default = null; @@ -151,6 +160,22 @@ let description = "Peers linked to the interface."; type = with types; listOf (submodule peerOpts); }; + + extraOptions = mkOption { + type = with types; attrsOf (oneOf [ str int ]); + default = { }; + example = { + Jc = 5; + Jmin = 10; + Jmax = 42; + S1 = 60; + S2 = 90; + H4 = 12345; + }; + description = '' + Extra options to append to the interface section. Can be used to define AmneziaWG-specific options. + ''; + }; }; }; @@ -230,7 +255,7 @@ let writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}"); - generatePrivateKeyScript = privateKeyFile: '' + generatePrivateKeyScript = privateKeyFile: wgBin: '' set -e # If the parent dir does not already exist, create it. @@ -239,7 +264,7 @@ let if [ ! -f "${privateKeyFile}" ]; then # Write private key file with atomically-correct permissions. - (set -e; umask 077; wg genkey > "${privateKeyFile}") + (set -e; umask 077; ${wgBin} genkey > "${privateKeyFile}") fi ''; @@ -247,11 +272,19 @@ let assert assertMsg (values.configFile != null || ((values.privateKey != null) != (values.privateKeyFile != null))) "Only one of privateKey, configFile or privateKeyFile may be set"; assert assertMsg (values.generatePrivateKeyFile == false || values.privateKeyFile != null) "generatePrivateKeyFile requires privateKeyFile to be set"; let - generateKeyScriptFile = if values.generatePrivateKeyFile then writeScriptFile "generatePrivateKey.sh" (generatePrivateKeyScript values.privateKeyFile) else null; + wgBin = { + wireguard = "wg"; + amneziawg = "awg"; + }.${values.type}; + generateKeyScriptFile = + if values.generatePrivateKeyFile then + writeScriptFile "generatePrivateKey.sh" (generatePrivateKeyScript values.privateKeyFile wgBin) + else + null; preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null; postUp = - optional (values.privateKeyFile != null) "wg set ${name} private-key <(cat ${values.privateKeyFile})" ++ - (concatMap (peer: optional (peer.presharedKeyFile != null) "wg set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++ + optional (values.privateKeyFile != null) "${wgBin} set ${name} private-key <(cat ${values.privateKeyFile})" ++ + (concatMap (peer: optional (peer.presharedKeyFile != null) "${wgBin} set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})") values.peers) ++ optional (values.postUp != "") values.postUp; postUpFile = if postUp != [] then writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) else null; preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null; @@ -279,6 +312,7 @@ let optionalString (postUpFile != null) "PostUp = ${postUpFile}\n" + optionalString (preDownFile != null) "PreDown = ${preDownFile}\n" + optionalString (postDownFile != null) "PostDown = ${postDownFile}\n" + + concatLines (mapAttrsToList (n: v: "${n} = ${toString v}") values.extraOptions) + concatMapStringsSep "\n" (peer: assert assertMsg (!((peer.presharedKeyFile != null) && (peer.presharedKey != null))) "Only one of presharedKey or presharedKeyFile may be set"; "[Peer]\n" + @@ -304,7 +338,10 @@ let wantedBy = optional values.autostart "multi-user.target"; environment.DEVICE = name; path = [ - pkgs.wireguard-tools + { + wireguard = pkgs.wireguard-tools; + amneziawg = pkgs.amneziawg-tools; + }.${values.type} config.networking.firewall.package # iptables or nftables config.networking.resolvconf.package # openresolv or systemd ]; @@ -315,11 +352,11 @@ let }; script = '' - ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe wireguard"} + ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe ${values.type}"} ${optionalString (values.configFile != null) '' cp ${values.configFile} ${configPath} ''} - wg-quick up ${configPath} + ${wgBin}-quick up ${configPath} ''; serviceConfig = { @@ -328,7 +365,7 @@ let }; preStop = '' - wg-quick down ${configPath} + ${wgBin}-quick down ${configPath} ''; }; in { @@ -360,8 +397,12 @@ in { ###### implementation config = mkIf (cfg.interfaces != {}) { - boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; - environment.systemPackages = [ pkgs.wireguard-tools ]; + boot.extraModulePackages = + optional (any (x: x.type == "wireguard") (attrValues cfg.interfaces) && (versionOlder kernel.kernel.version "5.6")) kernel.wireguard + ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) kernel.amneziawg; + environment.systemPackages = + optional (any (x: x.type == "wireguard") (attrValues cfg.interfaces)) pkgs.wireguard-tools + ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) pkgs.amneziawg-tools; systemd.services = mapAttrs' generateUnit cfg.interfaces; # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend). diff --git a/nixos/modules/services/networking/wireguard-networkd.nix b/nixos/modules/services/networking/wireguard-networkd.nix index 60d86ee43bef..be069328c66f 100644 --- a/nixos/modules/services/networking/wireguard-networkd.nix +++ b/nixos/modules/services/networking/wireguard-networkd.nix @@ -189,6 +189,10 @@ in assertion = interface.interfaceNamespace == null; message = "networking.wireguard.interfaces.${name}.interfaceNamespace cannot be used with networkd."; } + { + assertion = interface.type == "wireguard"; + message = "networking.wireguard.interfaces.${name}.type value must be \"wireguard\" when used with networkd."; + } ] ++ flip concatMap interface.ips (ip: [ # IP assertions diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix index 5d9bcc9e767a..b8c65f96d9ab 100644 --- a/nixos/modules/services/networking/wireguard.nix +++ b/nixos/modules/services/networking/wireguard.nix @@ -15,6 +15,15 @@ let options = { + type = mkOption { + example = "amneziawg"; + default = "wireguard"; + type = types.enum ["wireguard" "amneziawg"]; + description = '' + The type of the interface. Currently only "wireguard" and "amneziawg" are supported. + ''; + }; + ips = mkOption { example = [ "192.168.2.1/24" ]; default = []; @@ -206,6 +215,22 @@ let ::: ''; }; + + extraOptions = mkOption { + type = with types; attrsOf (oneOf [ str int ]); + default = { }; + example = { + Jc = 5; + Jmin = 10; + Jmax = 42; + S1 = 60; + S2 = 90; + H4 = 12345; + }; + description = '' + Extra options to append to the interface section. Can be used to define AmneziaWG-specific options. + ''; + }; }; }; @@ -346,6 +371,16 @@ let }; + wgBins = { + wireguard = "wg"; + amneziawg = "awg"; + }; + + wgPackages = { + wireguard = pkgs.wireguard-tools; + amneziawg = pkgs.amneziawg-tools; + }; + generateKeyServiceUnit = name: values: assert values.generatePrivateKeyFile; nameValuePair "wireguard-${name}-key" @@ -354,7 +389,7 @@ let wantedBy = [ "wireguard-${name}.service" ]; requiredBy = [ "wireguard-${name}.service" ]; before = [ "wireguard-${name}.service" ]; - path = with pkgs; [ wireguard-tools ]; + path = [ wgPackages.${values.type} ]; serviceConfig = { Type = "oneshot"; @@ -370,7 +405,7 @@ let if [ ! -f "${values.privateKeyFile}" ]; then # Write private key file with atomically-correct permissions. - (set -e; umask 077; wg genkey > "${values.privateKeyFile}") + (set -e; umask 077; ${wgBins.${values.type}} genkey > "${values.privateKeyFile}") fi ''; }; @@ -395,7 +430,7 @@ let src = interfaceCfg.socketNamespace; dst = interfaceCfg.interfaceNamespace; ip = nsWrap "ip" src dst; - wg = nsWrap "wg" src dst; + wg = nsWrap wgBins.${interfaceCfg.type} src dst; dynamicEndpointRefreshSeconds = dynamicRefreshSeconds interfaceCfg peer; dynamicRefreshEnabled = dynamicEndpointRefreshSeconds != 0; # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds` @@ -412,7 +447,7 @@ let wantedBy = [ "wireguard-${interfaceName}.service" ]; environment.DEVICE = interfaceName; environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity"; - path = with pkgs; [ iproute2 wireguard-tools ]; + path = with pkgs; [ iproute2 wgPackages.${interfaceCfg.type} ]; serviceConfig = if !dynamicRefreshEnabled @@ -501,7 +536,7 @@ let dst = values.interfaceNamespace; ipPreMove = nsWrap "ip" src null; ipPostMove = nsWrap "ip" src dst; - wg = nsWrap "wg" src dst; + wg = nsWrap wgBins.${values.type} src dst; ns = if dst == "init" then "1" else dst; in @@ -512,7 +547,7 @@ let wants = [ "network.target" ]; before = [ "network.target" ]; environment.DEVICE = name; - path = with pkgs; [ kmod iproute2 wireguard-tools ]; + path = with pkgs; [ kmod iproute2 wgPackages.${values.type} ]; serviceConfig = { Type = "oneshot"; @@ -520,10 +555,10 @@ let }; script = concatStringsSep "\n" ( - optional (!config.boot.isContainer) "modprobe wireguard || true" + optional (!config.boot.isContainer) "modprobe ${values.type} || true" ++ [ values.preSetup - ''${ipPreMove} link add dev "${name}" type wireguard'' + ''${ipPreMove} link add dev "${name}" type ${values.type}'' ] ++ optional (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"'' ++ optional (values.mtu != null) ''${ipPostMove} link set "${name}" mtu ${toString values.mtu}'' @@ -535,6 +570,7 @@ let [ ''${wg} set "${name}" private-key "${privKey}"'' ] ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"'' ++ optional (values.fwMark != null) ''fwmark "${values.fwMark}"'' + ++ mapAttrsToList (k: v: ''${toLower k} "${toString v}"'') values.extraOptions )) ''${ipPostMove} link set up dev "${name}"'' values.postSetup @@ -554,6 +590,9 @@ let ns = last nsList; in if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd; + + usingWg = any (x: x.type == "wireguard") (attrValues cfg.interfaces); + usingAwg = any (x: x.type == "amneziawg") (attrValues cfg.interfaces); in { @@ -628,9 +667,11 @@ in message = "networking.wireguard.interfaces.${interfaceName} peer «${peer.publicKey}» has both presharedKey and presharedKeyFile set, but only one can be used."; }) all_peers; - boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; - boot.kernelModules = [ "wireguard" ]; - environment.systemPackages = [ pkgs.wireguard-tools ]; + boot.extraModulePackages = + optional (usingWg && (versionOlder kernel.kernel.version "5.6")) kernel.wireguard + ++ optional usingAwg kernel.amneziawg; + boot.kernelModules = optional usingWg "wireguard" ++ optional usingAwg "amneziawg"; + environment.systemPackages = optional usingWg pkgs.wireguard-tools ++ optional usingAwg pkgs.amneziawg-tools; systemd.services = mkIf (!cfg.useNetworkd) ( (mapAttrs' generateInterfaceUnit cfg.interfaces) diff --git a/nixos/tests/wireguard/amneziawg-quick.nix b/nixos/tests/wireguard/amneziawg-quick.nix new file mode 100644 index 000000000000..0433233a2f96 --- /dev/null +++ b/nixos/tests/wireguard/amneziawg-quick.nix @@ -0,0 +1,125 @@ +import ../make-test-python.nix ( + { + pkgs, + lib, + kernelPackages ? null, + nftables ? false, + ... + }: + let + wg-snakeoil-keys = import ./snakeoil-keys.nix; + peer = import ./make-peer.nix { inherit lib; }; + commonConfig = { + boot.kernelPackages = lib.mkIf (kernelPackages != null) kernelPackages; + networking.nftables.enable = nftables; + # Make sure iptables doesn't work with nftables enabled + boot.blacklistedKernelModules = lib.mkIf nftables [ "nft_compat" ]; + }; + extraOptions = { + Jc = 5; + Jmin = 10; + Jmax = 42; + S1 = 60; + S2 = 90; + }; + in + { + name = "amneziawg-quick"; + meta = with pkgs.lib.maintainers; { + maintainers = [ + averyanalex + azahi + ]; + }; + + nodes = { + peer0 = peer { + ip4 = "192.168.0.1"; + ip6 = "fd00::1"; + extraConfig = lib.mkMerge [ + commonConfig + { + networking.firewall.allowedUDPPorts = [ 23542 ]; + networking.wg-quick.interfaces.wg0 = { + type = "amneziawg"; + + address = [ + "10.23.42.1/32" + "fc00::1/128" + ]; + listenPort = 23542; + + inherit (wg-snakeoil-keys.peer0) privateKey; + + peers = lib.singleton { + allowedIPs = [ + "10.23.42.2/32" + "fc00::2/128" + ]; + + inherit (wg-snakeoil-keys.peer1) publicKey; + }; + + dns = [ + "10.23.42.2" + "fc00::2" + "wg0" + ]; + + inherit extraOptions; + }; + } + ]; + }; + + peer1 = peer { + ip4 = "192.168.0.2"; + ip6 = "fd00::2"; + extraConfig = lib.mkMerge [ + commonConfig + { + networking.useNetworkd = true; + networking.wg-quick.interfaces.wg0 = { + type = "amneziawg"; + + address = [ + "10.23.42.2/32" + "fc00::2/128" + ]; + inherit (wg-snakeoil-keys.peer1) privateKey; + + peers = lib.singleton { + allowedIPs = [ + "0.0.0.0/0" + "::/0" + ]; + endpoint = "192.168.0.1:23542"; + persistentKeepalive = 25; + + inherit (wg-snakeoil-keys.peer0) publicKey; + }; + + dns = [ + "10.23.42.1" + "fc00::1" + "wg0" + ]; + + inherit extraOptions; + }; + } + ]; + }; + }; + + testScript = '' + start_all() + + peer0.wait_for_unit("wg-quick-wg0.service") + peer1.wait_for_unit("wg-quick-wg0.service") + + peer1.succeed("ping -c5 fc00::1") + peer1.succeed("ping -c5 10.23.42.1") + ''; + } +) diff --git a/nixos/tests/wireguard/amneziawg.nix b/nixos/tests/wireguard/amneziawg.nix new file mode 100644 index 000000000000..40ab0e002fec --- /dev/null +++ b/nixos/tests/wireguard/amneziawg.nix @@ -0,0 +1,111 @@ +import ../make-test-python.nix ( + { + pkgs, + lib, + kernelPackages ? null, + ... + }: + let + wg-snakeoil-keys = import ./snakeoil-keys.nix; + peer = (import ./make-peer.nix) { inherit lib; }; + extraOptions = { + Jc = 5; + Jmin = 10; + Jmax = 42; + S1 = 60; + S2 = 90; + }; + in + { + name = "amneziawg"; + meta = with pkgs.lib.maintainers; { + maintainers = [ + averyanalex + azahi + ]; + }; + + nodes = { + peer0 = peer { + ip4 = "192.168.0.1"; + ip6 = "fd00::1"; + extraConfig = { + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; + networking.firewall.allowedUDPPorts = [ 23542 ]; + networking.wireguard.interfaces.wg0 = { + type = "amneziawg"; + ips = [ + "10.23.42.1/32" + "fc00::1/128" + ]; + listenPort = 23542; + + inherit (wg-snakeoil-keys.peer0) privateKey; + + peers = lib.singleton { + allowedIPs = [ + "10.23.42.2/32" + "fc00::2/128" + ]; + + inherit (wg-snakeoil-keys.peer1) publicKey; + }; + + inherit extraOptions; + }; + }; + }; + + peer1 = peer { + ip4 = "192.168.0.2"; + ip6 = "fd00::2"; + extraConfig = { + boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; }; + networking.wireguard.interfaces.wg0 = { + type = "amneziawg"; + ips = [ + "10.23.42.2/32" + "fc00::2/128" + ]; + listenPort = 23542; + allowedIPsAsRoutes = false; + + inherit (wg-snakeoil-keys.peer1) privateKey; + + peers = lib.singleton { + allowedIPs = [ + "0.0.0.0/0" + "::/0" + ]; + endpoint = "192.168.0.1:23542"; + persistentKeepalive = 25; + + inherit (wg-snakeoil-keys.peer0) publicKey; + }; + + postSetup = + let + inherit (pkgs) iproute2; + in + '' + ${iproute2}/bin/ip route replace 10.23.42.1/32 dev wg0 + ${iproute2}/bin/ip route replace fc00::1/128 dev wg0 + ''; + + inherit extraOptions; + }; + }; + }; + }; + + testScript = '' + start_all() + + peer0.wait_for_unit("wireguard-wg0.service") + peer1.wait_for_unit("wireguard-wg0.service") + + peer1.succeed("ping -c5 fc00::1") + peer1.succeed("ping -c5 10.23.42.1") + ''; + } +) diff --git a/nixos/tests/wireguard/default.nix b/nixos/tests/wireguard/default.nix index 16393f533bc7..1202e48b297c 100644 --- a/nixos/tests/wireguard/default.nix +++ b/nixos/tests/wireguard/default.nix @@ -10,10 +10,12 @@ with pkgs.lib; let tests = let callTest = p: args: import p ({ inherit system pkgs; } // args); in { basic = callTest ./basic.nix; + amneziawg = callTest ./amneziawg.nix; namespaces = callTest ./namespaces.nix; networkd = callTest ./networkd.nix; wg-quick = callTest ./wg-quick.nix; wg-quick-nftables = args: callTest ./wg-quick.nix ({ nftables = true; } // args); + amneziawg-quick = callTest ./amneziawg-quick.nix; generated = callTest ./generated.nix; dynamic-refresh = callTest ./dynamic-refresh.nix; dynamic-refresh-networkd = args: callTest ./dynamic-refresh.nix ({ useNetworkd = true; } // args);