nixpkgs/nixos/modules/services/networking/wireguard-networkd.nix

235 lines
8.4 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib) types;
inherit (lib.attrsets)
filterAttrs
mapAttrs
mapAttrs'
mapAttrsToList
nameValuePair
;
inherit (lib.lists)
concatMap
concatLists
filter
flatten
;
inherit (lib.modules) mkIf;
inherit (lib.options) literalExpression mkOption;
inherit (lib.strings) hasInfix replaceStrings;
inherit (lib.trivial) flip pipe;
removeNulls = filterAttrs (_: v: v != null);
escapeCredentialName = input: replaceStrings [ "\\" ] [ "_" ] input;
privateKeyCredential = interfaceName: escapeCredentialName "wireguard-${interfaceName}-private-key";
presharedKeyCredential =
interfaceName: peer: escapeCredentialName "wireguard-${interfaceName}-${peer.name}-preshared-key";
interfaceCredentials =
interfaceName: interface:
[ "${privateKeyCredential interfaceName}:${interface.privateKeyFile}" ]
++ pipe interface.peers [
(filter (peer: peer.presharedKeyFile != null))
(map (peer: "${presharedKeyCredential interfaceName peer}:${peer.presharedKeyFile}"))
];
generateNetdev =
name: interface:
nameValuePair "40-${name}" {
netdevConfig = removeNulls {
Kind = "wireguard";
Name = name;
MTUBytes = interface.mtu;
};
wireguardConfig = removeNulls {
PrivateKey = "@${privateKeyCredential name}";
ListenPort = interface.listenPort;
FirewallMark = interface.fwMark;
RouteTable = if interface.allowedIPsAsRoutes then interface.table else null;
RouteMetric = interface.metric;
};
wireguardPeers = map (generateWireguardPeer name) interface.peers;
};
generateWireguardPeer =
interfaceName: peer:
removeNulls {
PublicKey = peer.publicKey;
PresharedKey =
if peer.presharedKeyFile == null then null else "@${presharedKeyCredential interfaceName peer}";
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/11103
# 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.";
}
{
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
{
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) // {
systemd-networkd.serviceConfig.LoadCredential = flatten (
mapAttrsToList interfaceCredentials cfg.interfaces
);
};
};
}