nixos/{wg-quick,wireguard}: add AmneziaWG support (#341663)

Azat Bahawi 0589bd30 96b88c68

Changed files
+346 -22
nixos
+52 -11
nixos/modules/services/networking/wg-quick.nix
···
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;
···
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.
+
'';
+
};
};
};
···
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.
···
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
'';
···
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;
···
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" +
···
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
];
···
};
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 = {
···
};
preStop = ''
-
wg-quick down ${configPath}
+
${wgBin}-quick down ${configPath}
'';
};
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).
+4
nixos/modules/services/networking/wireguard-networkd.nix
···
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
+52 -11
nixos/modules/services/networking/wireguard.nix
···
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 = [];
···
:::
'';
};
+
+
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.
+
'';
+
};
};
};
···
};
+
wgBins = {
+
wireguard = "wg";
+
amneziawg = "awg";
+
};
+
+
wgPackages = {
+
wireguard = pkgs.wireguard-tools;
+
amneziawg = pkgs.amneziawg-tools;
+
};
+
generateKeyServiceUnit = name: values:
assert values.generatePrivateKeyFile;
nameValuePair "wireguard-${name}-key"
···
wantedBy = [ "wireguard-${name}.service" ];
requiredBy = [ "wireguard-${name}.service" ];
before = [ "wireguard-${name}.service" ];
-
path = with pkgs; [ wireguard-tools ];
+
path = [ wgPackages.${values.type} ];
serviceConfig = {
Type = "oneshot";
···
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
'';
};
···
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`
···
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
···
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
···
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";
···
};
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}''
···
[ ''${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
···
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
{
···
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)
+125
nixos/tests/wireguard/amneziawg-quick.nix
···
+
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")
+
'';
+
}
+
)
+111
nixos/tests/wireguard/amneziawg.nix
···
+
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")
+
'';
+
}
+
)
+2
nixos/tests/wireguard/default.nix
···
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);