at 25.11-pre 16 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8with lib; 9let 10 cfg = config.networking.wg-quick; 11 12 kernel = config.boot.kernelPackages; 13 14 # interface options 15 16 interfaceOpts = 17 { ... }: 18 { 19 options = { 20 21 type = mkOption { 22 example = "amneziawg"; 23 default = "wireguard"; 24 type = types.enum [ 25 "wireguard" 26 "amneziawg" 27 ]; 28 description = '' 29 The type of the interface. Currently only "wireguard" and "amneziawg" are supported. 30 ''; 31 }; 32 33 configFile = mkOption { 34 example = "/secret/wg0.conf"; 35 default = null; 36 type = with types; nullOr str; 37 description = '' 38 wg-quick .conf file, describing the interface. 39 Using this option can be a useful means of configuring WireGuard if 40 one has an existing .conf file. 41 This overrides any other configuration interface configuration options. 42 See wg-quick manpage for more details. 43 ''; 44 }; 45 46 address = mkOption { 47 example = [ "192.168.2.1/24" ]; 48 default = [ ]; 49 type = with types; listOf str; 50 description = "The IP addresses of the interface."; 51 }; 52 53 autostart = mkOption { 54 description = "Whether to bring up this interface automatically during boot."; 55 default = true; 56 example = false; 57 type = types.bool; 58 }; 59 60 dns = mkOption { 61 example = [ "192.168.2.2" ]; 62 default = [ ]; 63 type = with types; listOf str; 64 description = "The IP addresses of DNS servers to configure."; 65 }; 66 67 privateKey = mkOption { 68 example = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 69 type = with types; nullOr str; 70 default = null; 71 description = '' 72 Base64 private key generated by {command}`wg genkey`. 73 74 Warning: Consider using privateKeyFile instead if you do not 75 want to store the key in the world-readable Nix store. 76 ''; 77 }; 78 79 generatePrivateKeyFile = mkOption { 80 default = false; 81 type = types.bool; 82 description = '' 83 Automatically generate a private key with 84 {command}`wg genkey`, at the privateKeyFile location. 85 ''; 86 }; 87 88 privateKeyFile = mkOption { 89 example = "/private/wireguard_key"; 90 type = with types; nullOr str; 91 default = null; 92 description = '' 93 Private key file as generated by {command}`wg genkey`. 94 ''; 95 }; 96 97 listenPort = mkOption { 98 default = null; 99 type = with types; nullOr int; 100 example = 51820; 101 description = '' 102 16-bit port for listening. Optional; if not specified, 103 automatically generated based on interface name. 104 ''; 105 }; 106 107 preUp = mkOption { 108 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"''; 109 default = ""; 110 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 111 description = '' 112 Commands called at the start of the interface setup. 113 ''; 114 }; 115 116 preDown = mkOption { 117 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"''; 118 default = ""; 119 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 120 description = '' 121 Command called before the interface is taken down. 122 ''; 123 }; 124 125 postUp = mkOption { 126 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns add foo"''; 127 default = ""; 128 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 129 description = '' 130 Commands called after the interface setup. 131 ''; 132 }; 133 134 postDown = mkOption { 135 example = literalExpression ''"''${pkgs.iproute2}/bin/ip netns del foo"''; 136 default = ""; 137 type = with types; coercedTo (listOf str) (concatStringsSep "\n") lines; 138 description = '' 139 Command called after the interface is taken down. 140 ''; 141 }; 142 143 table = mkOption { 144 example = "main"; 145 default = null; 146 type = with types; nullOr str; 147 description = '' 148 The kernel routing table to add this interface's 149 associated routes to. Setting this is useful for e.g. policy routing 150 ("ip rule") or virtual routing and forwarding ("ip vrf"). Both 151 numeric table IDs and table names (/etc/rt_tables) can be used. 152 Defaults to "main". 153 ''; 154 }; 155 156 mtu = mkOption { 157 example = 1248; 158 default = null; 159 type = with types; nullOr int; 160 description = '' 161 If not specified, the MTU is automatically determined 162 from the endpoint addresses or the system default route, which is usually 163 a sane choice. However, to manually specify an MTU to override this 164 automatic discovery, this value may be specified explicitly. 165 ''; 166 }; 167 168 peers = mkOption { 169 default = [ ]; 170 description = "Peers linked to the interface."; 171 type = with types; listOf (submodule peerOpts); 172 }; 173 174 extraOptions = mkOption { 175 type = 176 with types; 177 attrsOf (oneOf [ 178 str 179 int 180 ]); 181 default = { }; 182 example = { 183 Jc = 5; 184 Jmin = 10; 185 Jmax = 42; 186 S1 = 60; 187 S2 = 90; 188 H4 = 12345; 189 }; 190 description = '' 191 Extra options to append to the interface section. Can be used to define AmneziaWG-specific options. 192 ''; 193 }; 194 }; 195 }; 196 197 # peer options 198 199 peerOpts = { 200 options = { 201 publicKey = mkOption { 202 example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 203 type = types.str; 204 description = "The base64 public key to the peer."; 205 }; 206 207 presharedKey = mkOption { 208 default = null; 209 example = "rVXs/Ni9tu3oDBLS4hOyAUAa1qTWVA3loR8eL20os3I="; 210 type = with types; nullOr str; 211 description = '' 212 Base64 preshared key generated by {command}`wg genpsk`. 213 Optional, and may be omitted. This option adds an additional layer of 214 symmetric-key cryptography to be mixed into the already existing 215 public-key cryptography, for post-quantum resistance. 216 217 Warning: Consider using presharedKeyFile instead if you do not 218 want to store the key in the world-readable Nix store. 219 ''; 220 }; 221 222 presharedKeyFile = mkOption { 223 default = null; 224 example = "/private/wireguard_psk"; 225 type = with types; nullOr str; 226 description = '' 227 File pointing to preshared key as generated by {command}`wg genpsk`. 228 Optional, and may be omitted. This option adds an additional layer of 229 symmetric-key cryptography to be mixed into the already existing 230 public-key cryptography, for post-quantum resistance. 231 ''; 232 }; 233 234 allowedIPs = mkOption { 235 example = [ 236 "10.192.122.3/32" 237 "10.192.124.1/24" 238 ]; 239 type = with types; listOf str; 240 description = '' 241 List of IP (v4 or v6) addresses with CIDR masks from 242 which this peer is allowed to send incoming traffic and to which 243 outgoing traffic for this peer is directed. The catch-all 0.0.0.0/0 may 244 be specified for matching all IPv4 addresses, and ::/0 may be specified 245 for matching all IPv6 addresses.''; 246 }; 247 248 endpoint = mkOption { 249 default = null; 250 example = "demo.wireguard.io:12913"; 251 type = with types; nullOr str; 252 description = '' 253 Endpoint IP or hostname of the peer, followed by a colon, 254 and then a port number of the peer.''; 255 }; 256 257 persistentKeepalive = mkOption { 258 default = null; 259 type = with types; nullOr int; 260 example = 25; 261 description = '' 262 This is optional and is by default off, because most 263 users will not need it. It represents, in seconds, between 1 and 65535 264 inclusive, how often to send an authenticated empty packet to the peer, 265 for the purpose of keeping a stateful firewall or NAT mapping valid 266 persistently. For example, if the interface very rarely sends traffic, 267 but it might at anytime receive traffic from a peer, and it is behind 268 NAT, the interface might benefit from having a persistent keepalive 269 interval of 25 seconds; however, most users will not need this.''; 270 }; 271 }; 272 }; 273 274 writeScriptFile = name: text: ((pkgs.writeShellScriptBin name text) + "/bin/${name}"); 275 276 generatePrivateKeyScript = privateKeyFile: wgBin: '' 277 set -e 278 279 # If the parent dir does not already exist, create it. 280 # Otherwise, does nothing, keeping existing permissions intact. 281 mkdir -p --mode 0755 "${dirOf privateKeyFile}" 282 283 if [ ! -f "${privateKeyFile}" ]; then 284 # Write private key file with atomically-correct permissions. 285 (set -e; umask 077; ${wgBin} genkey > "${privateKeyFile}") 286 fi 287 ''; 288 289 generateUnit = 290 name: values: 291 assert assertMsg ( 292 values.configFile != null || ((values.privateKey != null) != (values.privateKeyFile != null)) 293 ) "Only one of privateKey, configFile or privateKeyFile may be set"; 294 assert assertMsg ( 295 values.generatePrivateKeyFile == false || values.privateKeyFile != null 296 ) "generatePrivateKeyFile requires privateKeyFile to be set"; 297 let 298 wgBin = 299 { 300 wireguard = "wg"; 301 amneziawg = "awg"; 302 } 303 .${values.type}; 304 generateKeyScriptFile = 305 if values.generatePrivateKeyFile then 306 writeScriptFile "generatePrivateKey.sh" (generatePrivateKeyScript values.privateKeyFile wgBin) 307 else 308 null; 309 preUpFile = if values.preUp != "" then writeScriptFile "preUp.sh" values.preUp else null; 310 postUp = 311 optional ( 312 values.privateKeyFile != null 313 ) "${wgBin} set ${name} private-key <(cat ${values.privateKeyFile})" 314 ++ (concatMap ( 315 peer: 316 optional ( 317 peer.presharedKeyFile != null 318 ) "${wgBin} set ${name} peer ${peer.publicKey} preshared-key <(cat ${peer.presharedKeyFile})" 319 ) values.peers) 320 ++ optional (values.postUp != "") values.postUp; 321 postUpFile = 322 if postUp != [ ] then 323 writeScriptFile "postUp.sh" (concatMapStringsSep "\n" (line: line) postUp) 324 else 325 null; 326 preDownFile = if values.preDown != "" then writeScriptFile "preDown.sh" values.preDown else null; 327 postDownFile = 328 if values.postDown != "" then writeScriptFile "postDown.sh" values.postDown else null; 329 configDir = pkgs.writeTextFile { 330 name = "config-${name}"; 331 executable = false; 332 destination = "/${name}.conf"; 333 text = 334 '' 335 [interface] 336 ${concatMapStringsSep "\n" (address: "Address = ${address}") values.address} 337 ${concatMapStringsSep "\n" (dns: "DNS = ${dns}") values.dns} 338 '' 339 + optionalString (values.table != null) "Table = ${values.table}\n" 340 + optionalString (values.mtu != null) "MTU = ${toString values.mtu}\n" 341 + optionalString (values.privateKey != null) "PrivateKey = ${values.privateKey}\n" 342 + optionalString (values.listenPort != null) "ListenPort = ${toString values.listenPort}\n" 343 + optionalString (generateKeyScriptFile != null) "PreUp = ${generateKeyScriptFile}\n" 344 + optionalString (preUpFile != null) "PreUp = ${preUpFile}\n" 345 + optionalString (postUpFile != null) "PostUp = ${postUpFile}\n" 346 + optionalString (preDownFile != null) "PreDown = ${preDownFile}\n" 347 + optionalString (postDownFile != null) "PostDown = ${postDownFile}\n" 348 + concatLines (mapAttrsToList (n: v: "${n} = ${toString v}") values.extraOptions) 349 + concatMapStringsSep "\n" ( 350 peer: 351 assert assertMsg ( 352 !((peer.presharedKeyFile != null) && (peer.presharedKey != null)) 353 ) "Only one of presharedKey or presharedKeyFile may be set"; 354 "[Peer]\n" 355 + "PublicKey = ${peer.publicKey}\n" 356 + optionalString (peer.presharedKey != null) "PresharedKey = ${peer.presharedKey}\n" 357 + optionalString (peer.endpoint != null) "Endpoint = ${peer.endpoint}\n" 358 + optionalString ( 359 peer.persistentKeepalive != null 360 ) "PersistentKeepalive = ${toString peer.persistentKeepalive}\n" 361 + optionalString (peer.allowedIPs != [ ]) "AllowedIPs = ${concatStringsSep "," peer.allowedIPs}\n" 362 ) values.peers; 363 }; 364 configPath = 365 if values.configFile != null then 366 # This uses bind-mounted private tmp folder (/tmp/systemd-private-***) 367 "/tmp/${name}.conf" 368 else 369 "${configDir}/${name}.conf"; 370 in 371 nameValuePair "wg-quick-${name}" { 372 description = "wg-quick WireGuard Tunnel - ${name}"; 373 requires = [ "network-online.target" ]; 374 after = [ 375 "network.target" 376 "network-online.target" 377 ]; 378 wantedBy = optional values.autostart "multi-user.target"; 379 environment.DEVICE = name; 380 path = [ 381 { 382 wireguard = pkgs.wireguard-tools; 383 amneziawg = pkgs.amneziawg-tools; 384 } 385 .${values.type} 386 config.networking.firewall.package # iptables or nftables 387 config.networking.resolvconf.package # openresolv or systemd 388 ]; 389 390 serviceConfig = { 391 Type = "oneshot"; 392 RemainAfterExit = true; 393 }; 394 395 script = '' 396 ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe ${values.type}"} 397 ${optionalString (values.configFile != null) '' 398 cp ${values.configFile} ${configPath} 399 ''} 400 ${wgBin}-quick up ${configPath} 401 ''; 402 403 serviceConfig = { 404 # Used to privately store renamed copies of external config files during activation 405 PrivateTmp = true; 406 }; 407 408 preStop = '' 409 ${wgBin}-quick down ${configPath} 410 ''; 411 }; 412in 413{ 414 415 ###### interface 416 417 options = { 418 networking.wg-quick = { 419 interfaces = mkOption { 420 description = "Wireguard interfaces."; 421 default = { }; 422 example = { 423 wg0 = { 424 address = [ "192.168.20.4/24" ]; 425 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 426 peers = [ 427 { 428 allowedIPs = [ "192.168.20.1/32" ]; 429 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 430 endpoint = "demo.wireguard.io:12913"; 431 } 432 ]; 433 }; 434 }; 435 type = with types; attrsOf (submodule interfaceOpts); 436 }; 437 }; 438 }; 439 440 ###### implementation 441 442 config = mkIf (cfg.interfaces != { }) { 443 boot.extraModulePackages = 444 optional ( 445 any (x: x.type == "wireguard") (attrValues cfg.interfaces) 446 && (versionOlder kernel.kernel.version "5.6") 447 ) kernel.wireguard 448 ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) kernel.amneziawg; 449 environment.systemPackages = 450 optional (any (x: x.type == "wireguard") (attrValues cfg.interfaces)) pkgs.wireguard-tools 451 ++ optional (any (x: x.type == "amneziawg") (attrValues cfg.interfaces)) pkgs.amneziawg-tools; 452 systemd.services = mapAttrs' generateUnit cfg.interfaces; 453 454 # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend). 455 systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false; 456 457 # WireGuard interfaces should be ignored in determining whether the network is online. 458 systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces; 459 }; 460}