Skip to content

Commit

Permalink
nixos/wireguard-networkd: init
Browse files Browse the repository at this point in the history
Adds a networkd backend for the networking.wireguard options.
  • Loading branch information
Majiir committed Dec 8, 2024
1 parent f3f364e commit a5de365
Show file tree
Hide file tree
Showing 6 changed files with 456 additions and 17 deletions.
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
207 changes: 207 additions & 0 deletions nixos/modules/services/networking/wireguard-networkd.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
{
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;
};
}
71 changes: 54 additions & 17 deletions nixos/modules/services/networking/wireguard.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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.
'';
};

Expand Down Expand Up @@ -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.
:::
'';
};
};

};
Expand Down Expand Up @@ -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.
'';
};

Expand Down Expand Up @@ -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.
:::
'';
};

Expand Down Expand Up @@ -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 =
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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
''}
'';
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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 = {
Expand Down Expand Up @@ -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);
}
);

Expand Down
3 changes: 3 additions & 0 deletions nixos/tests/wireguard/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit a5de365

Please sign in to comment.