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