1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib) types;
10 inherit (lib.attrsets)
11 filterAttrs
12 mapAttrs
13 mapAttrs'
14 mapAttrsToList
15 nameValuePair
16 ;
17 inherit (lib.lists)
18 concatMap
19 concatLists
20 filter
21 flatten
22 ;
23 inherit (lib.modules) mkIf;
24 inherit (lib.options) literalExpression mkOption;
25 inherit (lib.strings) hasInfix replaceStrings;
26 inherit (lib.trivial) flip pipe;
27
28 removeNulls = filterAttrs (_: v: v != null);
29
30 escapeCredentialName = input: replaceStrings [ "\\" ] [ "_" ] input;
31
32 privateKeyCredential = interfaceName: escapeCredentialName "wireguard-${interfaceName}-private-key";
33 presharedKeyCredential =
34 interfaceName: peer: escapeCredentialName "wireguard-${interfaceName}-${peer.name}-preshared-key";
35
36 interfaceCredentials =
37 interfaceName: interface:
38 [ "${privateKeyCredential interfaceName}:${interface.privateKeyFile}" ]
39 ++ pipe interface.peers [
40 (filter (peer: peer.presharedKeyFile != null))
41 (map (peer: "${presharedKeyCredential interfaceName peer}:${peer.presharedKeyFile}"))
42 ];
43
44 generateNetdev =
45 name: interface:
46 nameValuePair "40-${name}" {
47 netdevConfig = removeNulls {
48 Kind = "wireguard";
49 Name = name;
50 MTUBytes = interface.mtu;
51 };
52 wireguardConfig = removeNulls {
53 PrivateKey = "@${privateKeyCredential name}";
54 ListenPort = interface.listenPort;
55 FirewallMark = interface.fwMark;
56 RouteTable = if interface.allowedIPsAsRoutes then interface.table else null;
57 RouteMetric = interface.metric;
58 };
59 wireguardPeers = map (generateWireguardPeer name) interface.peers;
60 };
61
62 generateWireguardPeer =
63 interfaceName: peer:
64 removeNulls {
65 PublicKey = peer.publicKey;
66 PresharedKey =
67 if peer.presharedKeyFile == null then null else "@${presharedKeyCredential interfaceName peer}";
68 AllowedIPs = peer.allowedIPs;
69 Endpoint = peer.endpoint;
70 PersistentKeepalive = peer.persistentKeepalive;
71 };
72
73 generateNetwork = name: interface: {
74 matchConfig.Name = name;
75 address = interface.ips;
76 };
77
78 cfg = config.networking.wireguard;
79
80 refreshEnabledInterfaces = filterAttrs (
81 name: interface: interface.dynamicEndpointRefreshSeconds != 0
82 ) cfg.interfaces;
83
84 generateRefreshTimer =
85 name: interface:
86 nameValuePair "wireguard-dynamic-refresh-${name}" {
87 partOf = [ "wireguard-dynamic-refresh-${name}.service" ];
88 wantedBy = [ "timers.target" ];
89 description = "Wireguard dynamic endpoint refresh (${name}) timer";
90 timerConfig.OnBootSec = interface.dynamicEndpointRefreshSeconds;
91 timerConfig.OnUnitInactiveSec = interface.dynamicEndpointRefreshSeconds;
92 };
93
94 generateRefreshService =
95 name: interface:
96 nameValuePair "wireguard-dynamic-refresh-${name}" {
97 description = "Wireguard dynamic endpoint refresh (${name})";
98 after = [ "network-online.target" ];
99 wants = [ "network-online.target" ];
100 path = with pkgs; [
101 iproute2
102 systemd
103 ];
104 # networkd doesn't provide a mechanism for refreshing endpoints.
105 # See: https://github.com/systemd/systemd/issues/9911
106 # This hack does the job but takes down the whole interface to do it.
107 script = ''
108 ip link delete ${name}
109 networkctl reload
110 '';
111 };
112
113in
114{
115 meta.maintainers = [ lib.maintainers.majiir ];
116
117 options.networking.wireguard = {
118 useNetworkd = mkOption {
119 default = config.networking.useNetworkd;
120 defaultText = literalExpression "config.networking.useNetworkd";
121 type = types.bool;
122 description = ''
123 Whether to use networkd as the network configuration backend for
124 Wireguard instead of the legacy script-based system.
125
126 ::: {.warning}
127 Some options have slightly different behavior with the networkd and
128 script-based backends. Check the documentation for each Wireguard
129 option you use before enabling this option.
130 :::
131 '';
132 };
133 };
134
135 config = mkIf (cfg.enable && cfg.useNetworkd) {
136
137 # TODO: Some of these options may be possible to support in networkd.
138 #
139 # privateKey and presharedKey are trivial to support, but we deliberately
140 # don't in order to discourage putting secrets in the /nix store.
141 #
142 # generatePrivateKeyFile can be supported if we can order a service before
143 # networkd configures interfaces. There is also a systemd feature request
144 # for key generation: https://github.com/systemd/systemd/issues/14282
145 #
146 # preSetup, postSetup, preShutdown and postShutdown may be possible, but
147 # networkd is not likely to support script hooks like this directly. See:
148 # https://github.com/systemd/systemd/issues/11629
149 #
150 # socketNamespace and interfaceNamespace can be implemented once networkd
151 # supports setting a netdev's namespace. See:
152 # https://github.com/systemd/systemd/issues/11103
153 # https://github.com/systemd/systemd/pull/14915
154
155 assertions = concatLists (
156 flip mapAttrsToList cfg.interfaces (
157 name: interface:
158 [
159 # Interface assertions
160 {
161 assertion = interface.privateKey == null;
162 message = "networking.wireguard.interfaces.${name}.privateKey cannot be used with networkd. Use privateKeyFile instead.";
163 }
164 {
165 assertion = !interface.generatePrivateKeyFile;
166 message = "networking.wireguard.interfaces.${name}.generatePrivateKeyFile cannot be used with networkd.";
167 }
168 {
169 assertion = interface.preSetup == "";
170 message = "networking.wireguard.interfaces.${name}.preSetup cannot be used with networkd.";
171 }
172 {
173 assertion = interface.postSetup == "";
174 message = "networking.wireguard.interfaces.${name}.postSetup cannot be used with networkd.";
175 }
176 {
177 assertion = interface.preShutdown == "";
178 message = "networking.wireguard.interfaces.${name}.preShutdown cannot be used with networkd.";
179 }
180 {
181 assertion = interface.postShutdown == "";
182 message = "networking.wireguard.interfaces.${name}.postShutdown cannot be used with networkd.";
183 }
184 {
185 assertion = interface.socketNamespace == null;
186 message = "networking.wireguard.interfaces.${name}.socketNamespace cannot be used with networkd.";
187 }
188 {
189 assertion = interface.interfaceNamespace == null;
190 message = "networking.wireguard.interfaces.${name}.interfaceNamespace cannot be used with networkd.";
191 }
192 {
193 assertion = interface.type == "wireguard";
194 message = "networking.wireguard.interfaces.${name}.type value must be \"wireguard\" when used with networkd.";
195 }
196 ]
197 ++ flip concatMap interface.ips (ip: [
198 # IP assertions
199 {
200 assertion = hasInfix "/" ip;
201 message = "networking.wireguard.interfaces.${name}.ips value \"${ip}\" requires a subnet (e.g. 192.0.2.1/32) with networkd.";
202 }
203 ])
204 ++ flip concatMap interface.peers (peer: [
205 # Peer assertions
206 {
207 assertion = peer.presharedKey == null;
208 message = "networking.wireguard.interfaces.${name}.peers[].presharedKey cannot be used with networkd. Use presharedKeyFile instead.";
209 }
210 {
211 assertion = peer.dynamicEndpointRefreshSeconds == null;
212 message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshSeconds cannot be used with networkd. Use networking.wireguard.interfaces.${name}.dynamicEndpointRefreshSeconds instead.";
213 }
214 {
215 assertion = peer.dynamicEndpointRefreshRestartSeconds == null;
216 message = "networking.wireguard.interfaces.${name}.peers[].dynamicEndpointRefreshRestartSeconds cannot be used with networkd.";
217 }
218 ])
219 )
220 );
221
222 systemd.network = {
223 enable = true;
224 netdevs = mapAttrs' generateNetdev cfg.interfaces;
225 networks = mapAttrs generateNetwork cfg.interfaces;
226 };
227
228 systemd.timers = mapAttrs' generateRefreshTimer refreshEnabledInterfaces;
229 systemd.services = (mapAttrs' generateRefreshService refreshEnabledInterfaces) // {
230 systemd-networkd.serviceConfig.LoadCredential = flatten (
231 mapAttrsToList interfaceCredentials cfg.interfaces
232 );
233 };
234 };
235}