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