at 23.11-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 = [ 277 pkgs.wireguard-tools 278 config.networking.firewall.package # iptables or nftables 279 config.networking.resolvconf.package # openresolv or systemd 280 ]; 281 282 serviceConfig = { 283 Type = "oneshot"; 284 RemainAfterExit = true; 285 }; 286 287 script = '' 288 ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe wireguard"} 289 ${optionalString (values.configFile != null) '' 290 cp ${values.configFile} ${configPath} 291 ''} 292 wg-quick up ${configPath} 293 ''; 294 295 serviceConfig = { 296 # Used to privately store renamed copies of external config files during activation 297 PrivateTmp = true; 298 }; 299 300 preStop = '' 301 wg-quick down ${configPath} 302 ''; 303 }; 304in { 305 306 ###### interface 307 308 options = { 309 networking.wg-quick = { 310 interfaces = mkOption { 311 description = lib.mdDoc "Wireguard interfaces."; 312 default = {}; 313 example = { 314 wg0 = { 315 address = [ "192.168.20.4/24" ]; 316 privateKey = "yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk="; 317 peers = [ 318 { allowedIPs = [ "192.168.20.1/32" ]; 319 publicKey = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg="; 320 endpoint = "demo.wireguard.io:12913"; } 321 ]; 322 }; 323 }; 324 type = with types; attrsOf (submodule interfaceOpts); 325 }; 326 }; 327 }; 328 329 330 ###### implementation 331 332 config = mkIf (cfg.interfaces != {}) { 333 boot.extraModulePackages = optional (versionOlder kernel.kernel.version "5.6") kernel.wireguard; 334 environment.systemPackages = [ pkgs.wireguard-tools ]; 335 systemd.services = mapAttrs' generateUnit cfg.interfaces; 336 337 # Prevent networkd from clearing the rules set by wg-quick when restarted (e.g. when waking up from suspend). 338 systemd.network.config.networkConfig.ManageForeignRoutingPolicyRules = mkDefault false; 339 340 # WireGuard interfaces should be ignored in determining whether the network is online. 341 systemd.network.wait-online.ignoredInterfaces = builtins.attrNames cfg.interfaces; 342 }; 343}