Unverified Commit afe27494 authored by misuzu's avatar misuzu Committed by GitHub
Browse files

nixos/wireguard-networkd: init (#259092)

parents d88cb592 b7390e36
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -102,6 +102,8 @@

- Cinnamon has been updated to 6.4.

- `networking.wireguard` now has an optional networkd backend. It is enabled by default when `networking.useNetworkd` is enabled, and it can be enabled alongside scripted networking with `networking.wireguard.useNetworkd`. Some `networking.wireguard` options have slightly different behavior with the networkd and script-based backends, documented in each option. Before upgrading, make sure the `privateKeyFile` and `presharedKeyFile` paths are readable by the `systemd-network` user if using the networkd backend.

- `services.avahi.ipv6` now defaults to true.

- `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.
+1 −0
Original line number Diff line number Diff line
@@ -1286,6 +1286,7 @@
  ./services/networking/wg-quick.nix
  ./services/networking/wgautomesh.nix
  ./services/networking/wireguard.nix
  ./services/networking/wireguard-networkd.nix
  ./services/networking/wpa_supplicant.nix
  ./services/networking/wstunnel.nix
  ./services/networking/x2goserver.nix
+207 −0
Original line number Diff line number Diff line
{
  config,
  lib,
  pkgs,
  ...
}:

let
  inherit (lib) types;
  inherit (lib.attrsets)
    filterAttrs
    mapAttrs
    mapAttrs'
    mapAttrsToList
    nameValuePair
    ;
  inherit (lib.lists) concatMap concatLists;
  inherit (lib.modules) mkIf;
  inherit (lib.options) literalExpression mkOption;
  inherit (lib.strings) hasInfix;
  inherit (lib.trivial) flip;

  removeNulls = filterAttrs (_: v: v != null);

  generateNetdev =
    name: interface:
    nameValuePair "40-${name}" {
      netdevConfig = removeNulls {
        Kind = "wireguard";
        Name = name;
        MTUBytes = interface.mtu;
      };
      wireguardConfig = removeNulls {
        PrivateKeyFile = interface.privateKeyFile;
        ListenPort = interface.listenPort;
        FirewallMark = interface.fwMark;
        RouteTable = if interface.allowedIPsAsRoutes then interface.table else null;
        RouteMetric = interface.metric;
      };
      wireguardPeers = map generateWireguardPeer interface.peers;
    };

  generateWireguardPeer =
    peer:
    removeNulls {
      PublicKey = peer.publicKey;
      PresharedKeyFile = peer.presharedKeyFile;
      AllowedIPs = peer.allowedIPs;
      Endpoint = peer.endpoint;
      PersistentKeepalive = peer.persistentKeepalive;
    };

  generateNetwork = name: interface: {
    matchConfig.Name = name;
    address = interface.ips;
  };

  cfg = config.networking.wireguard;

  refreshEnabledInterfaces = filterAttrs (
    name: interface: interface.dynamicEndpointRefreshSeconds != 0
  ) cfg.interfaces;

  generateRefreshTimer =
    name: interface:
    nameValuePair "wireguard-dynamic-refresh-${name}" {
      partOf = [ "wireguard-dynamic-refresh-${name}.service" ];
      wantedBy = [ "timers.target" ];
      description = "Wireguard dynamic endpoint refresh (${name}) timer";
      timerConfig.OnBootSec = interface.dynamicEndpointRefreshSeconds;
      timerConfig.OnUnitInactiveSec = interface.dynamicEndpointRefreshSeconds;
    };

  generateRefreshService =
    name: interface:
    nameValuePair "wireguard-dynamic-refresh-${name}" {
      description = "Wireguard dynamic endpoint refresh (${name})";
      after = [ "network-online.target" ];
      wants = [ "network-online.target" ];
      path = with pkgs; [
        iproute2
        systemd
      ];
      # networkd doesn't provide a mechanism for refreshing endpoints.
      # See: https://github.com/systemd/systemd/issues/9911
      # This hack does the job but takes down the whole interface to do it.
      script = ''
        ip link delete ${name}
        networkctl reload
      '';
    };

in
{
  meta.maintainers = [ lib.maintainers.majiir ];

  options.networking.wireguard = {
    useNetworkd = mkOption {
      default = config.networking.useNetworkd;
      defaultText = literalExpression "config.networking.useNetworkd";
      type = types.bool;
      description = ''
        Whether to use networkd as the network configuration backend for
        Wireguard instead of the legacy script-based system.

        ::: {.warning}
        Some options have slightly different behavior with the networkd and
        script-based backends. Check the documentation for each Wireguard
        option you use before enabling this option.
        :::
      '';
    };
  };

  config = mkIf (cfg.enable && cfg.useNetworkd) {

    # TODO: Some of these options may be possible to support in networkd.
    #
    # privateKey and presharedKey are trivial to support, but we deliberately
    # don't in order to discourage putting secrets in the /nix store.
    #
    # generatePrivateKeyFile can be supported if we can order a service before
    # networkd configures interfaces. There is also a systemd feature request
    # for key generation: https://github.com/systemd/systemd/issues/14282
    #
    # preSetup, postSetup, preShutdown and postShutdown may be possible, but
    # networkd is not likely to support script hooks like this directly. See:
    # https://github.com/systemd/systemd/issues/11629
    #
    # socketNamespace and interfaceNamespace can be implemented once networkd
    # supports setting a netdev's namespace. See:
    # https://github.com/systemd/systemd/issues/11629
    # https://github.com/systemd/systemd/pull/14915

    assertions = concatLists (
      flip mapAttrsToList cfg.interfaces (
        name: interface:
        [
          # Interface assertions
          {
            assertion = interface.privateKey == null;
            message = "networking.wireguard.interfaces.${name}.privateKey cannot be used with networkd. Use privateKeyFile instead.";
          }
          {
            assertion = !interface.generatePrivateKeyFile;
            message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile cannot be used with networkd.";
          }
          {
            assertion = interface.preSetup == "";
            message = "networking.wireguard.interfaces.${name}.preSetup cannot be used with networkd.";
          }
          {
            assertion = interface.postSetup == "";
            message = "networking.wireguard.interfaces.${name}.postSetup cannot be used with networkd.";
          }
          {
            assertion = interface.preShutdown == "";
            message = "networking.wireguard.interfaces.${name}.preShutdown cannot be used with networkd.";
          }
          {
            assertion = interface.postShutdown == "";
            message = "networking.wireguard.interfaces.${name}.postShutdown cannot be used with networkd.";
          }
          {
            assertion = interface.socketNamespace == null;
            message = "networking.wireguard.interfaces.${name}.socketNamespace cannot be used with networkd.";
          }
          {
            assertion = interface.interfaceNamespace == null;
            message = "networking.wireguard.interfaces.${name}.interfaceNamespace cannot be used with networkd.";
          }
        ]
        ++ flip concatMap interface.ips (ip: [
          # IP assertions
          {
            assertion = hasInfix "/" ip;
            message = "networking.wireguard.interfaces.${name}.ips value \"${ip}\" requires a subnet (e.g. 192.0.2.1/32) with networkd.";
          }
        ])
        ++ flip concatMap interface.peers (peer: [
          # Peer assertions
          {
            assertion = peer.presharedKey == null;
            message = "networking.wireguard.interfaces.${name}.peers[].presharedKey cannot be used with networkd. Use presharedKeyFile instead.";
          }
          {
            assertion = peer.dynamicEndpointRefreshSeconds == null;
            message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshSeconds cannot be used with networkd. Use networking.wireguard.interfaces.${name}.dynamicEndpointRefreshSeconds instead.";
          }
          {
            assertion = peer.dynamicEndpointRefreshRestartSeconds == null;
            message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshRestartSeconds cannot be used with networkd.";
          }
        ])
      )
    );

    systemd.network = {
      enable = true;
      netdevs = mapAttrs' generateNetdev cfg.interfaces;
      networks = mapAttrs generateNetwork cfg.interfaces;
    };

    systemd.timers = mapAttrs' generateRefreshTimer refreshEnabledInterfaces;
    systemd.services = mapAttrs' generateRefreshService refreshEnabledInterfaces;
  };
}
+54 −17
Original line number Diff line number Diff line
@@ -49,6 +49,9 @@ let
        default = null;
        description = ''
          Private key file as generated by {command}`wg genkey`.

          When {option}`networking.wireguard.useNetworkd` is enabled, this file
          must be readable by the `systemd-network` user.
        '';
      };

@@ -182,6 +185,28 @@ let
          Set the metric of routes related to this Wireguard interface.
        '';
      };

      dynamicEndpointRefreshSeconds = mkOption {
        default = 0;
        example = 300;
        type = with types; int;
        description = ''
          Periodically refresh the endpoint hostname or address for all peers.
          Allows WireGuard to notice DNS and IPv4/IPv6 connectivity changes.
          This option can be set or overridden for individual peers.

          Setting this to `0` disables periodic refresh.

          ::: {.warning}
          When {option}`networking.wireguard.useNetworkd` is enabled, this
          option deletes the Wireguard interface and brings it back up by
          reconfiguring the network with `networkctl reload` on every refresh.
          This could have adverse effects on your network and cause brief
          connectivity blips. See [systemd/systemd#9911](https://github.com/systemd/systemd/issues/9911)
          for an upstream feature request that can make this less hacky.
          :::
        '';
      };
    };

  };
@@ -234,6 +259,9 @@ let
          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.

          When {option}`networking.wireguard.useNetworkd` is enabled, this file
          must be readable by the `systemd-network` user.
        '';
      };

@@ -269,15 +297,21 @@ let
      };

      dynamicEndpointRefreshSeconds = mkOption {
        default = 0;
        default = null;
        defaultText = literalExpression "config.networking.wireguard.interfaces.<name>.dynamicEndpointRefreshSeconds";
        example = 5;
        type = with types; int;
        type = with types; nullOr int;
        description = ''
          Periodically re-execute the `wg` utility every
          this many seconds in order to let WireGuard notice DNS / hostname
          changes.

          Setting this to `0` disables periodic reexecution.

          ::: {.note}
          This peer-level setting is not available when {option}`networking.wireguard.useNetworkd`
          is enabled. The interface-level setting may be used instead.
          :::
        '';
      };

@@ -349,6 +383,11 @@ let
    in
      "wireguard-${interfaceName}-peer-${peerName}${refreshSuffix}";

  dynamicRefreshSeconds = interfaceCfg: peer:
    if peer.dynamicEndpointRefreshSeconds != null
    then peer.dynamicEndpointRefreshSeconds
    else interfaceCfg.dynamicEndpointRefreshSeconds;

  generatePeerUnit = { interfaceName, interfaceCfg, peer }:
    let
      psk =
@@ -359,7 +398,8 @@ let
      dst = interfaceCfg.interfaceNamespace;
      ip = nsWrap "ip" src dst;
      wg = nsWrap "wg" src dst;
      dynamicRefreshEnabled = peer.dynamicEndpointRefreshSeconds != 0;
      dynamicEndpointRefreshSeconds = dynamicRefreshSeconds interfaceCfg peer;
      dynamicRefreshEnabled = 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.
@@ -395,7 +435,7 @@ let
                Restart = "always";
                RestartSec = if null != peer.dynamicEndpointRefreshRestartSeconds
                             then peer.dynamicEndpointRefreshRestartSeconds
                             else peer.dynamicEndpointRefreshSeconds;
                             else dynamicEndpointRefreshSeconds;
              };
        unitConfig = lib.optionalAttrs dynamicRefreshEnabled {
          StartLimitIntervalSec = 0;
@@ -419,13 +459,13 @@ let
          ${wg_setup}
          ${route_setup}

          ${optionalString (peer.dynamicEndpointRefreshSeconds != 0) ''
          ${optionalString (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}";
              sleep "${toString dynamicEndpointRefreshSeconds}";
            done
          ''}
        '';
@@ -445,7 +485,7 @@ let
  # the target is required to start new peer units when they are added
  generateInterfaceTarget = name: values:
    let
      mkPeerUnit = peer: (peerUnitServiceName name peer.name (peer.dynamicEndpointRefreshSeconds != 0)) + ".service";
      mkPeerUnit = peer: (peerUnitServiceName name peer.name (dynamicRefreshSeconds values peer != 0)) + ".service";
    in
    nameValuePair "wireguard-${name}"
      rec {
@@ -530,9 +570,10 @@ in
        description = ''
          Whether to enable WireGuard.

          Please note that {option}`systemd.network.netdevs` has more features
          and is better maintained. When building new things, it is advised to
          use that instead.
          ::: {.note}
          By default, this module is powered by a script-based backend. You can
          enable the networkd backend with {option}`networking.wireguard.useNetworkd`.
          :::
        '';
        type = types.bool;
        # 2019-05-25: Backwards compatibility.
@@ -544,10 +585,6 @@ in
      interfaces = mkOption {
        description = ''
          WireGuard interfaces.

          Please note that {option}`systemd.network.netdevs` has more features
          and is better maintained. When building new things, it is advised to
          use that instead.
        '';
        default = {};
        example = {
@@ -597,13 +634,13 @@ in
    boot.kernelModules = [ "wireguard" ];
    environment.systemPackages = [ pkgs.wireguard-tools ];

    systemd.services =
    systemd.services = mkIf (!cfg.useNetworkd) (
      (mapAttrs' generateInterfaceUnit cfg.interfaces)
      // (listToAttrs (map generatePeerUnit all_peers))
      // (mapAttrs' generateKeyServiceUnit
      (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces));
      (filterAttrs (name: value: value.generatePrivateKeyFile) cfg.interfaces)));

      systemd.targets = mapAttrs' generateInterfaceTarget cfg.interfaces;
      systemd.targets = mkIf (!cfg.useNetworkd) (mapAttrs' generateInterfaceTarget cfg.interfaces);
    }
  );

+3 −0
Original line number Diff line number Diff line
@@ -11,9 +11,12 @@ let
  tests = let callTest = p: args: import p ({ inherit system pkgs; } // args); in {
    basic = callTest ./basic.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);
    generated = callTest ./generated.nix;
    dynamic-refresh = callTest ./dynamic-refresh.nix;
    dynamic-refresh-networkd = args: callTest ./dynamic-refresh.nix ({ useNetworkd = true; } // args);
  };
in

Loading