diff options
author | Alyssa Ross <hi@alyssa.is> | 2022-05-31 09:59:33 +0000 |
---|---|---|
committer | Alyssa Ross <hi@alyssa.is> | 2022-05-31 09:59:57 +0000 |
commit | 9ff36293d1e428cd7bf03e8d4b03611b6d361c28 (patch) | |
tree | 1ab51a42b868c55b83f6ccdb80371b9888739dd9 /nixos/modules/services/networking/wireguard.nix | |
parent | 1c4fcd0d4b0541e674ee56ace1053e23e562cc80 (diff) | |
parent | ddc3c396a51918043bb0faa6f676abd9562be62c (diff) | |
download | nixpkgs-archive.tar nixpkgs-archive.tar.gz nixpkgs-archive.tar.bz2 nixpkgs-archive.tar.lz nixpkgs-archive.tar.xz nixpkgs-archive.tar.zst nixpkgs-archive.zip |
Last good Nixpkgs for Weston+nouveau? archive
I came this commit hash to terwiz[m] on IRC, who is trying to figure out what the last version of Spectrum that worked on their NUC with Nvidia graphics is.
Diffstat (limited to 'nixos/modules/services/networking/wireguard.nix')
-rw-r--r-- | nixos/modules/services/networking/wireguard.nix | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix new file mode 100644 index 00000000000..7cd44b2f8a0 --- /dev/null +++ b/nixos/modules/services/networking/wireguard.nix @@ -0,0 +1,503 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + + cfg = config.networking.wireguard; + opt = options.networking.wireguard; + + kernel = config.boot.kernelPackages; + + # interface options + + interfaceOpts = { ... }: { + + options = { + + ips = mkOption { + example = [ "192.168.2.1/24" ]; + default = []; + type = with types; listOf str; + description = "The IP addresses of the interface."; + }; + + privateKey = mkOption { + example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; + type = with types; nullOr str; + default = null; + description = '' + Base64 private key generated by <command>wg genkey</command>. + + Warning: Consider using privateKeyFile instead if you do not + want to store the key in the world-readable Nix store. + ''; + }; + + generatePrivateKeyFile = mkOption { + default = false; + type = types.bool; + description = '' + Automatically generate a private key with + <command>wg genkey</command>, at the privateKeyFile location. + ''; + }; + + privateKeyFile = mkOption { + example = "/private/wireguard_key"; + type = with types; nullOr str; + default = null; + description = '' + Private key file as generated by <command>wg genkey</command>. + ''; + }; + + listenPort = mkOption { + default = null; + type = with types; nullOr int; + example = 51820; + description = '' + 16-bit port for listening. Optional; if not specified, + automatically generated based on interface name. + ''; + }; + + preSetup = mkOption { + example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"''; + default = ""; + type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; + description = '' + Commands called at the start of the interface setup. + ''; + }; + + postSetup = mkOption { + example = literalExpression '' + '''printf "nameserver 10.200.100.1" | ''${pkgs.openresolv}/bin/resolvconf -a wg0 -m 0''' + ''; + default = ""; + type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; + description = "Commands called at the end of the interface setup."; + }; + + postShutdown = mkOption { + example = literalExpression ''"''${pkgs.openresolv}/bin/resolvconf -d wg0"''; + default = ""; + type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; + description = "Commands called after shutting down the interface."; + }; + + table = mkOption { + default = "main"; + type = types.str; + description = '' + The kernel routing table to add this interface's + associated routes to. Setting this is useful for e.g. policy routing + ("ip rule") or virtual routing and forwarding ("ip vrf"). Both + numeric table IDs and table names (/etc/rt_tables) can be used. + Defaults to "main". + ''; + }; + + peers = mkOption { + default = []; + description = "Peers linked to the interface."; + type = with types; listOf (submodule peerOpts); + }; + + allowedIPsAsRoutes = mkOption { + example = false; + default = true; + type = types.bool; + description = '' + Determines whether to add allowed IPs as routes or not. + ''; + }; + + socketNamespace = mkOption { + default = null; + type = with types; nullOr str; + example = "container"; + description = ''The pre-existing network namespace in which the + WireGuard interface is created, and which retains the socket even if the + interface is moved via <option>interfaceNamespace</option>. When + <literal>null</literal>, the interface is created in the init namespace. + See <link + xlink:href="https://www.wireguard.com/netns/">documentation</link>. + ''; + }; + + interfaceNamespace = mkOption { + default = null; + type = with types; nullOr str; + example = "init"; + description = ''The pre-existing network namespace the WireGuard + interface is moved to. The special value <literal>init</literal> means + the init namespace. When <literal>null</literal>, the interface is not + moved. + See <link + xlink:href="https://www.wireguard.com/netns/">documentation</link>. + ''; + }; + }; + + }; + + # peer options + + peerOpts = { + + options = { + + publicKey = mkOption { + example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; + type = types.str; + description = "The base64 public key of the peer."; + }; + + presharedKey = mkOption { + default = null; + example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I="; + type = with types; nullOr str; + description = '' + Base64 preshared key generated by <command>wg genpsk</command>. + Optional, and may be omitted. This option adds an additional layer of + symmetric-key cryptography to be mixed into the already existing + public-key cryptography, for post-quantum resistance. + + Warning: Consider using presharedKeyFile instead if you do not + want to store the key in the world-readable Nix store. + ''; + }; + + presharedKeyFile = mkOption { + default = null; + example = "/private/wireguard_psk"; + type = with types; nullOr str; + description = '' + File pointing to preshared key as generated by <command>wg genpsk</command>. + Optional, and may be omitted. This option adds an additional layer of + symmetric-key cryptography to be mixed into the already existing + public-key cryptography, for post-quantum resistance. + ''; + }; + + allowedIPs = mkOption { + example = [ "10.192.122.3/32" "10.192.124.1/24" ]; + type = with types; listOf str; + description = ''List of IP (v4 or v6) addresses with CIDR masks from + which this peer is allowed to send incoming traffic and to which + outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may + be specified for matching all IPv4 addresses, and ::/0 may be specified + for matching all IPv6 addresses.''; + }; + + endpoint = mkOption { + default = null; + example = "demo.wireguard.io:12913"; + type = with types; nullOr str; + description = ''Endpoint IP or hostname of the peer, followed by a colon, + and then a port number of the peer. + + Warning for endpoints with changing IPs: + The WireGuard kernel side cannot perform DNS resolution. + Thus DNS resolution is done once by the <literal>wg</literal> userspace + utility, when setting up WireGuard. Consequently, if the IP address + behind the name changes, WireGuard will not notice. + This is especially common for dynamic-DNS setups, but also applies to + any other DNS-based setup. + If you do not use IP endpoints, you likely want to set + <option>networking.wireguard.dynamicEndpointRefreshSeconds</option> + to refresh the IPs periodically. + ''; + }; + + dynamicEndpointRefreshSeconds = mkOption { + default = 0; + example = 5; + type = with types; int; + description = '' + Periodically re-execute the <literal>wg</literal> utility every + this many seconds in order to let WireGuard notice DNS / hostname + changes. + + Setting this to <literal>0</literal> disables periodic reexecution. + ''; + }; + + persistentKeepalive = mkOption { + default = null; + type = with types; nullOr int; + example = 25; + description = ''This is optional and is by default off, because most + users will not need it. It represents, in seconds, between 1 and 65535 + inclusive, how often to send an authenticated empty packet to the peer, + for the purpose of keeping a stateful firewall or NAT mapping valid + persistently. For example, if the interface very rarely sends traffic, + but it might at anytime receive traffic from a peer, and it is behind + NAT, the interface might benefit from having a persistent keepalive + interval of 25 seconds; however, most users will not need this.''; + }; + + }; + + }; + + generateKeyServiceUnit = name: values: + assert values.generatePrivateKeyFile; + nameValuePair "wireguard-${name}-key" + { + description = "WireGuard Tunnel - ${name} - Key Generator"; + wantedBy = [ "wireguard-${name}.service" ]; + requiredBy = [ "wireguard-${name}.service" ]; + before = [ "wireguard-${name}.service" ]; + path = with pkgs; [ wireguard-tools ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + set -e + + # If the parent dir does not already exist, create it. + # Otherwise, does nothing, keeping existing permisions intact. + mkdir -p --mode 0755 "${dirOf values.privateKeyFile}" + + if [ ! -f "${values.privateKeyFile}" ]; then + # Write private key file with atomically-correct permissions. + (set -e; umask 077; wg genkey > "${values.privateKeyFile}") + fi + ''; + }; + + peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled: + let + keyToUnitName = replaceChars + [ "/" "-" " " "+" "=" ] + [ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ]; + unitName = keyToUnitName publicKey; + refreshSuffix = optionalString dynamicRefreshEnabled "-refresh"; + in + "wireguard-${interfaceName}-peer-${unitName}${refreshSuffix}"; + + generatePeerUnit = { interfaceName, interfaceCfg, peer }: + let + psk = + if peer.presharedKey != null + then pkgs.writeText "wg-psk" peer.presharedKey + else peer.presharedKeyFile; + src = interfaceCfg.socketNamespace; + dst = interfaceCfg.interfaceNamespace; + ip = nsWrap "ip" src dst; + wg = nsWrap "wg" src dst; + dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0; + # We generate a different name (a `-refresh` suffix) when `dynamicEndpointRefreshSeconds` + # to avoid that the same service switches `Type` (`oneshot` vs `simple`), + # with the intent to make scripting more obvious. + serviceName = peerUnitServiceName interfaceName peer.publicKey dynamicRefreshEnabled; + in nameValuePair serviceName + { + description = "WireGuard Peer - ${interfaceName} - ${peer.publicKey}"; + requires = [ "wireguard-${interfaceName}.service" ]; + after = [ "wireguard-${interfaceName}.service" ]; + wantedBy = [ "multi-user.target" "wireguard-${interfaceName}.service" ]; + environment.DEVICE = interfaceName; + environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity"; + path = with pkgs; [ iproute2 wireguard-tools ]; + + serviceConfig = + if !dynamicRefreshEnabled + then + { + Type = "oneshot"; + RemainAfterExit = true; + } + else + { + Type = "simple"; # re-executes 'wg' indefinitely + # Note that `Type = "oneshot"` services with `RemainAfterExit = true` + # cannot be used with systemd timers (see `man systemd.timer`), + # which is why `simple` with a loop is the best choice here. + # It also makes starting and stopping easiest. + }; + + script = let + wg_setup = concatStringsSep " " ( + [ ''${wg} set ${interfaceName} peer "${peer.publicKey}"'' ] + ++ optional (psk != null) ''preshared-key "${psk}"'' + ++ optional (peer.endpoint != null) ''endpoint "${peer.endpoint}"'' + ++ optional (peer.persistentKeepalive != null) ''persistent-keepalive "${toString peer.persistentKeepalive}"'' + ++ optional (peer.allowedIPs != []) ''allowed-ips "${concatStringsSep "," peer.allowedIPs}"'' + ); + route_setup = + optionalString interfaceCfg.allowedIPsAsRoutes + (concatMapStringsSep "\n" + (allowedIP: + ''${ip} route replace "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"'' + ) peer.allowedIPs); + in '' + ${wg_setup} + ${route_setup} + + ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) '' + # Re-execute 'wg' periodically to notice DNS / hostname changes. + # Note this will not time out on transient DNS failures such as DNS names + # because we have set 'WG_ENDPOINT_RESOLUTION_RETRIES=infinity'. + # Also note that 'wg' limits its maximum retry delay to 20 seconds as of writing. + while ${wg_setup}; do + sleep "${toString peer.dynamicEndpointRefreshSeconds}"; + done + ''} + ''; + + postStop = let + route_destroy = optionalString interfaceCfg.allowedIPsAsRoutes + (concatMapStringsSep "\n" + (allowedIP: + ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"'' + ) peer.allowedIPs); + in '' + ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove + ${route_destroy} + ''; + }; + + generateInterfaceUnit = name: values: + # exactly one way to specify the private key must be set + #assert (values.privateKey != null) != (values.privateKeyFile != null); + let privKey = if values.privateKeyFile != null then values.privateKeyFile else pkgs.writeText "wg-key" values.privateKey; + src = values.socketNamespace; + dst = values.interfaceNamespace; + ipPreMove = nsWrap "ip" src null; + ipPostMove = nsWrap "ip" src dst; + wg = nsWrap "wg" src dst; + ns = if dst == "init" then "1" else dst; + + in + nameValuePair "wireguard-${name}" + { + description = "WireGuard Tunnel - ${name}"; + requires = [ "network-online.target" ]; + after = [ "network.target" "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + environment.DEVICE = name; + path = with pkgs; [ kmod iproute2 wireguard-tools ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + ${optionalString (!config.boot.isContainer) "modprobe wireguard || true"} + + ${values.preSetup} + + ${ipPreMove} link add dev "${name}" type wireguard + ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''} + + ${concatMapStringsSep "\n" (ip: + ''${ipPostMove} address add "${ip}" dev "${name}"'' + ) values.ips} + + ${concatStringsSep " " ( + [ ''${wg} set "${name}" private-key "${privKey}"'' ] + ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"'' + )} + + ${ipPostMove} link set up dev "${name}" + + ${values.postSetup} + ''; + + postStop = '' + ${ipPostMove} link del dev "${name}" + ${values.postShutdown} + ''; + }; + + nsWrap = cmd: src: dst: + let + nsList = filter (ns: ns != null) [ src dst ]; + ns = last nsList; + in + if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd; +in + +{ + + ###### interface + + options = { + + networking.wireguard = { + + enable = mkOption { + description = "Whether to enable WireGuard."; + type = types.bool; + # 2019-05-25: Backwards compatibility. + default = cfg.interfaces != {}; + defaultText = literalExpression "config.${opt.interfaces} != { }"; + example = true; + }; + + interfaces = mkOption { + description = "WireGuard interfaces."; + default = {}; + example = { + wg0 = { + ips = [ "192.168.20.4/24" ]; + privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; + peers = [ + { allowedIPs = [ "192.168.20.1/32" ]; + publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; + endpoint = "demo.wireguard.io:12913"; } + ]; + }; + }; + type = with types; attrsOf (submodule interfaceOpts); + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable (let + all_peers = flatten + (mapAttrsToList (interfaceName: interfaceCfg: + map (peer: { inherit interfaceName interfaceCfg peer;}) interfaceCfg.peers + ) cfg.interfaces); + in { + + assertions = (attrValues ( + mapAttrs (name: value: { + assertion = (value.privateKey != null) != (value.privateKeyFile != null); + message = "Either networking.wireguard.interfaces.${name}.privateKey or networking.wireguard.interfaces.${name}.privateKeyFile must be set."; + }) cfg.interfaces)) + ++ (attrValues ( + mapAttrs (name: value: { + assertion = value.generatePrivateKeyFile -> (value.privateKey == null); + message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile must not be set if networking.wireguard.interfaces.${name}.privateKey is set."; + }) cfg.interfaces)) + ++ map ({ interfaceName, peer, ... }: { + assertion = (peer.presharedKey == null) || (peer.presharedKeyFile == null); + 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; + environment.systemPackages = [ pkgs.wireguard-tools ]; + + systemd.services = + (mapAttrs' generateInterfaceUnit cfg.interfaces) + // (listToAttrs (map generatePeerUnit all_peers)) + // (mapAttrs' generateKeyServiceUnit + (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces)); + + }); + +} |