diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml
index a5501533be7f..9ce24700686a 100644
--- a/nixos/doc/manual/release-notes/rl-2105.xml
+++ b/nixos/doc/manual/release-notes/rl-2105.xml
@@ -395,6 +395,15 @@
which is the new stable release. OpenAFS 1.6 was removed.
+
+
+ The WireGuard module gained a new option
+
+ that implements refreshing the IP of DNS-based endpoints periodically
+ (which WireGuard itself
+ cannot do).
+
+
MariaDB has been updated to 10.5.
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 043bce16e542..471f4bf8b33f 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -198,7 +198,32 @@ let
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.'';
+ 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 wg 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
+
+ to refresh the IPs periodically.
+ '';
+ };
+
+ dynamicEndpointRefreshSeconds = mkOption {
+ default = 0;
+ example = 5;
+ type = with types; 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.
+ '';
};
persistentKeepalive = mkOption {
@@ -259,12 +284,18 @@ let
'';
};
- generatePeerUnit = { interfaceName, interfaceCfg, peer }:
+ peerUnitServiceName = interfaceName: publicKey: dynamicRefreshEnabled:
let
keyToUnitName = replaceChars
[ "/" "-" " " "+" "=" ]
[ "-" "\\x2d" "\\x20" "\\x2b" "\\x3d" ];
- unitName = keyToUnitName peer.publicKey;
+ 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
@@ -273,7 +304,12 @@ let
dst = interfaceCfg.interfaceNamespace;
ip = nsWrap "ip" src dst;
wg = nsWrap "wg" src dst;
- in nameValuePair "wireguard-${interfaceName}-peer-${unitName}"
+ 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" ];
@@ -283,36 +319,59 @@ let
environment.WG_ENDPOINT_RESOLUTION_RETRIES = "infinity";
path = with pkgs; [ iproute2 wireguard-tools ];
- serviceConfig = {
- Type = "oneshot";
- RemainAfterExit = true;
- };
+ 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 = "${wg} set ${interfaceName} peer ${peer.publicKey}" +
- optionalString (psk != null) " preshared-key ${psk}" +
- optionalString (peer.endpoint != null) " endpoint ${peer.endpoint}" +
- optionalString (peer.persistentKeepalive != null) " persistent-keepalive ${toString peer.persistentKeepalive}" +
- optionalString (peer.allowedIPs != []) " allowed-ips ${concatStringsSep "," peer.allowedIPs}";
+ 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}"
+ ''${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}"
+ ''${ip} route delete "${allowedIP}" dev "${interfaceName}" table "${interfaceCfg.table}"''
) peer.allowedIPs);
in ''
- ${wg} set ${interfaceName} peer ${peer.publicKey} remove
+ ${wg} set "${interfaceName}" peer "${peer.publicKey}" remove
${route_destroy}
'';
};
@@ -348,23 +407,25 @@ let
${values.preSetup}
- ${ipPreMove} link add dev ${name} type wireguard
- ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) "${ipPreMove} link set ${name} netns ${ns}"}
+ ${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}"
+ ''${ipPostMove} address add "${ip}" dev "${name}"''
) values.ips}
- ${wg} set ${name} private-key ${privKey} ${
- optionalString (values.listenPort != null) " listen-port ${toString values.listenPort}"}
+ ${concatStringsSep " " (
+ [ ''${wg} set "${name}" private-key "${privKey}"'' ]
+ ++ optional (values.listenPort != null) ''listen-port "${toString values.listenPort}"''
+ )}
- ${ipPostMove} link set up dev ${name}
+ ${ipPostMove} link set up dev "${name}"
${values.postSetup}
'';
postStop = ''
- ${ipPostMove} link del dev ${name}
+ ${ipPostMove} link del dev "${name}"
${values.postShutdown}
'';
};
@@ -374,7 +435,7 @@ 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;
+ if (length nsList > 0 && ns != "init") then ''ip netns exec "${ns}" "${cmd}"'' else cmd;
in
{