summary refs log tree commit diff
path: root/nixos/modules/services/networking/wireguard.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/networking/wireguard.nix')
-rw-r--r--nixos/modules/services/networking/wireguard.nix503
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));
+
+  });
+
+}