diff --git a/lib/wireguard.nix b/lib/wireguard.nix index 52f1aae..4409ae6 100644 --- a/lib/wireguard.nix +++ b/lib/wireguard.nix @@ -6,30 +6,18 @@ inputs: final: prev: let attrValues concatLists concatMap - concatStringsSep - escapeShellArg filter - flatten flip genAttrs - mapAttrs' - nameValuePair partition - removeSuffix warn ; inherit (final.lib) net - concatAttrs types ; - - inherit - (final.lib.secrets) - rageDecryptArgs - ; in { lib = prev.lib @@ -66,15 +54,15 @@ in { in "wireguard-${wgName}-psks-${peer1}+${peer2}"; # All nodes that are part of this network - participatingNodes = - filter - (n: builtins.hasAttr wgName nodes.${n}.config.wireguard) - (attrNames nodes); + participatingNodes = filter (n: builtins.hasAttr wgName nodes.${n}.config.wireguard) ( + attrNames nodes + ); # Partition nodes by whether they are servers _participatingNodes_isServerPartition = - partition - (n: (wgCfgOf n).server.host != null) + partition ( + n: (wgCfgOf n).server.host != null + ) participatingNodes; participatingServerNodes = _participatingNodes_isServerPartition.right; @@ -83,43 +71,26 @@ in { # Maps all nodes that are part of this network to their addresses nodePeers = genAttrs participatingNodes (n: (wgCfgOf n).addresses); - externalPeerName = p: "external-${p}"; - - # Only peers that are defined as externalPeers on the given node. - # Prepends "external-" to their name. - externalPeersForNode = node: - mapAttrs' (p: nameValuePair (externalPeerName p)) (wgCfgOf node).server.externalPeers; - - # All peers that are defined as externalPeers on any node. - # Prepends "external-" to their name. - allExternalPeers = concatAttrs (map externalPeersForNode participatingNodes); - - # All peers that are part of this network - allPeers = nodePeers // allExternalPeers; - - # Concatenation of all external peer names names without any transformations. - externalPeerNamesRaw = concatMap (n: attrNames (wgCfgOf n).server.externalPeers) participatingNodes; - # A list of all occurring addresses. - usedAddresses = - concatMap (n: (wgCfgOf n).addresses) participatingNodes - ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); + usedAddresses = concatMap (n: (wgCfgOf n).addresses) participatingNodes; # A list of all occurring addresses, but only includes addresses that # are not assigned automatically. - explicitlyUsedAddresses = - flip concatMap participatingNodes - (n: - filter (x: !types.isLazyValue x) - (concatLists - (nodes.${n}.options.wireguard.type.nestedTypes.elemType.getSubOptions (wgCfgOf n)).addresses.definitions)) - ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); + explicitlyUsedAddresses = flip concatMap participatingNodes ( + n: + filter (x: !types.isLazyValue x) ( + concatLists + (nodes.${n}.options.wireguard.type.nestedTypes.elemType.getSubOptions (wgCfgOf n)) + .addresses + .definitions + ) + ); # The cidrv4 and cidrv6 of the network spanned by all participating peer addresses. # This also takes into account any reserved address ranges that should be part of the network. - networkAddresses = - net.cidr.merge (usedAddresses - ++ concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes); + networkAddresses = net.cidr.merge ( + usedAddresses ++ concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes + ); # The network spanning cidr addresses. The respective cidrv4 and cirdv6 are only # included if they exist. @@ -127,29 +98,30 @@ in { # The cidrv4 and cidrv6 of the network spanned by all reserved addresses only. # Used to determine automatically assigned addresses first. - spannedReservedNetwork = - net.cidr.merge (concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes); + spannedReservedNetwork = net.cidr.merge ( + concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes + ); # Assigns an ipv4 address from spannedReservedNetwork.cidrv4 # to each participant that has not explicitly specified an ipv4 address. - assignedIpv4Addresses = assert assertMsg - (spannedReservedNetwork.cidrv4 != null) + assignedIpv4Addresses = assert assertMsg (spannedReservedNetwork.cidrv4 != null) "Wireguard network '${wgName}': At least one participating node must reserve a cidrv4 address via `reservedAddresses` so that ipv4 addresses can be assigned automatically from that network."; - net.cidr.assignIps - spannedReservedNetwork.cidrv4 + net.cidr.assignIps spannedReservedNetwork.cidrv4 # Don't assign any addresses that are explicitly configured on other hosts - (filter (x: net.cidr.contains x spannedReservedNetwork.cidrv4) (filter net.ip.isv4 explicitlyUsedAddresses)) + (filter (x: net.cidr.contains x spannedReservedNetwork.cidrv4) ( + filter net.ip.isv4 explicitlyUsedAddresses + )) participatingNodes; # Assigns an ipv6 address from spannedReservedNetwork.cidrv6 # to each participant that has not explicitly specified an ipv6 address. - assignedIpv6Addresses = assert assertMsg - (spannedReservedNetwork.cidrv6 != null) + assignedIpv6Addresses = assert assertMsg (spannedReservedNetwork.cidrv6 != null) "Wireguard network '${wgName}': At least one participating node must reserve a cidrv6 address via `reservedAddresses` so that ipv6 addresses can be assigned automatically from that network."; - net.cidr.assignIps - spannedReservedNetwork.cidrv6 + net.cidr.assignIps spannedReservedNetwork.cidrv6 # Don't assign any addresses that are explicitly configured on other hosts - (filter (x: net.cidr.contains x spannedReservedNetwork.cidrv6) (filter net.ip.isv6 explicitlyUsedAddresses)) + (filter (x: net.cidr.contains x spannedReservedNetwork.cidrv6) ( + filter net.ip.isv6 explicitlyUsedAddresses + )) participatingNodes; # Appends / replaces the correct cidr length to the argument, @@ -160,45 +132,11 @@ in { then networkAddresses.cidrv6 else networkAddresses.cidrv4; in "${net.cidr.ip addr}/${toString (net.cidr.length relevantNetworkAddr)}"; - - # Creates a script that when executed outputs a wg-quick compatible configuration - # file for use with external peers. This is a script so we can access secrets without - # storing them in the nix-store. - wgQuickConfigScript = system: serverNode: extPeer: let - pkgs = userInputs.self.pkgs.${system}; - snCfg = wgCfgOf serverNode; - peerName = externalPeerName extPeer; - addresses = map toNetworkAddr snCfg.server.externalPeers.${extPeer}; - in - pkgs.writeShellScript "create-wg-conf-${wgName}-${serverNode}-${extPeer}" '' - privKey=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPrivateKeyPath peerName)}) \ - || { echo "error: Failed to decrypt!" >&2; exit 1; } - serverPsk=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPresharedKeyPath serverNode peerName)}) \ - || { echo "error: Failed to decrypt!" >&2; exit 1; } - - cat < ${lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub")} + ${pkgs.wireguard-tools}/bin/wg pubkey <<< "$priv" > ${ + lib.escapeShellArg (lib.removeSuffix ".age" file + ".pub") + } echo "$priv" ''; }; @@ -227,34 +223,26 @@ if isServer then # Always include all other server nodes. - map (serverNode: let - snCfg = wgCfgOf serverNode; - in { - PublicKey = builtins.readFile (peerPublicKeyPath serverNode); - PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName serverNode}.path; - AllowedIPs = serverAllowedIPs serverNode; - Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}"; - }) - (filterSelf participatingServerNodes) - # All our external peers - ++ mapAttrsToList (extPeer: ips: let - peerName = externalPeerName extPeer; - in { - PublicKey = builtins.readFile (peerPublicKeyPath peerName); - PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName peerName}.path; - AllowedIPs = map (net.cidr.make 128) ips; - # Connections to external peers should always be kept alive - PersistentKeepalive = 25; - }) - wgCfg.server.externalPeers + map ( + serverNode: let + snCfg = wgCfgOf serverNode; + in { + PublicKey = builtins.readFile (peerPublicKeyPath serverNode); + PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName serverNode}.path; + AllowedIPs = serverAllowedIPs serverNode; + Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}"; + } + ) (filterSelf participatingServerNodes) # All client nodes that have their via set to us. - ++ map (clientNode: let - clientCfg = wgCfgOf clientNode; - in { - PublicKey = builtins.readFile (peerPublicKeyPath clientNode); - PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName clientNode}.path; - AllowedIPs = map (net.cidr.make 128) clientCfg.addresses; - }) + ++ map ( + clientNode: let + clientCfg = wgCfgOf clientNode; + in { + PublicKey = builtins.readFile (peerPublicKeyPath clientNode); + PresharedKeyFile = config.age.secrets.${peerPresharedKeySecret nodeName clientNode}.path; + AllowedIPs = map (net.cidr.make 128) clientCfg.addresses; + } + ) ourClientNodes else # We are a client node, so only include our via server. @@ -286,189 +274,184 @@ in { options.wireguard = mkOption { default = {}; description = "Configures wireguard networks via systemd-networkd."; - type = types.lazyAttrsOf (types.submodule ({ - config, - name, - options, - ... - }: { - options = { - server = { - host = mkOption { - default = null; - type = types.nullOr types.str; - description = "The hostname or ip address which other peers can use to reach this host. No server functionality will be activated if set to null."; - }; - - port = mkOption { - default = 51820; - type = types.port; - description = "The port to listen on."; - }; - - openFirewall = mkOption { - default = false; - type = types.bool; - description = "Whether to open the firewall for the specified {option}`port`."; - }; - - externalPeers = mkOption { - type = types.attrsOf (types.listOf (types.net.ip-in config.addresses)); - default = {}; - example = {my-android-phone = ["10.0.0.97"];}; - description = '' - Allows defining an extra set of peers that should be added to this wireguard network, - but will not be managed by this flake. (e.g. phones) - - These external peers will only know this node as a peer, which will forward - their traffic to other members of the network if required. This requires - this node to act as a server. - ''; - }; - - reservedAddresses = mkOption { - type = types.listOf types.net.cidr; - default = []; - example = ["10.0.0.0/24" "fd00:cafe::/64"]; - description = '' - Allows defining extra CIDR network ranges that shall be reserved for this network. - Reservation means that those address spaces will be guaranteed to be included in - the spanned network, but no rules will be enforced as to who in the network may use them. - - By default, this module will try to allocate the smallest address space that includes - all network peers. If you know that there might be additional external peers added later, - it may be beneficial to reserve a bigger address space from the start to avoid having - to update existing external peers when the generated address space expands. - ''; - }; - }; - - client = { - via = mkOption { - default = null; - type = types.nullOr types.str; - description = '' - The server node via which to connect to the network. - No client functionality will be activated if set to null. - ''; - }; - - keepalive = mkOption { - default = true; - type = types.bool; - description = "Whether to keep this connection alive using PersistentKeepalive. Set to false only for networks where client and server IPs are stable."; - }; - }; - - priority = mkOption { - default = 40; - type = types.int; - description = "The order priority used when creating systemd netdev and network files."; - }; - - linkName = mkOption { - default = name; - type = types.str; - description = "The name for the created network interface."; - }; - - unitConfName = mkOption { - default = "${toString config.priority}-${config.linkName}"; - readOnly = true; - type = types.str; - description = '' - The name used for unit configuration files. This is a read-only option. - Access this if you want to add additional settings to the generated systemd units. - ''; - }; - - ipv4 = mkOption { - type = types.lazyOf types.net.ipv4; - default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv4Addresses.${nodeName}; - description = '' - The ipv4 address for this machine. If you do not set this explicitly, - a semi-stable ipv4 address will be derived automatically based on the - hostname of this machine. At least one participating server must reserve - a big-enough space of addresses by setting `reservedAddresses`. - See `net.cidr.assignIps` for more information on the algorithm. - ''; - }; - - ipv6 = mkOption { - type = types.lazyOf types.net.ipv6; - default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv6Addresses.${nodeName}; - description = '' - The ipv6 address for this machine. If you do not set this explicitly, - a semi-stable ipv6 address will be derived automatically based on the - hostname of this machine. At least one participating server must reserve - a big-enough space of addresses by setting `reservedAddresses`. - See `net.cidr.assignIps` for more information on the algorithm. - ''; - }; - - addresses = mkOption { - type = types.listOf (types.lazyOf types.net.ip); - default = [ - (head options.ipv4.definitions) - (head options.ipv6.definitions) - ]; - description = '' - The ip addresses (v4 and/or v6) to use for this machine. - The actual network cidr will automatically be derived from all network participants. - By default this will just include {option}`ipv4` and {option}`ipv6` as configured. - ''; - }; - - firewallRuleForAll = mkOption { - default = {}; - description = '' - Allows you to set specific firewall rules for traffic originating from any participant in this - wireguard network. A corresponding rule `wg--to-` will be created to easily expose - services to the network. - ''; - type = types.submodule { - options = { - allowedTCPPorts = mkOption { - type = types.listOf types.port; - default = []; - description = "Convenience option to open specific TCP ports for traffic from the network."; + type = types.lazyAttrsOf ( + types.submodule ( + { + config, + name, + options, + ... + }: { + options = { + server = { + host = mkOption { + default = null; + type = types.nullOr types.str; + description = "The hostname or ip address which other peers can use to reach this host. No server functionality will be activated if set to null."; }; - allowedUDPPorts = mkOption { - type = types.listOf types.port; + + port = mkOption { + default = 51820; + type = types.port; + description = "The port to listen on."; + }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = "Whether to open the firewall for the specified {option}`port`."; + }; + + reservedAddresses = mkOption { + type = types.listOf types.net.cidr; default = []; - description = "Convenience option to open specific UDP ports for traffic from the network."; + example = [ + "10.0.0.0/24" + "fd00:cafe::/64" + ]; + description = '' + Allows defining extra CIDR network ranges that shall be reserved for this network. + Reservation means that those address spaces will be guaranteed to be included in + the spanned network, but no rules will be enforced as to who in the network may use them. + + By default, this module will try to allocate the smallest address space that includes + all network peers. + ''; }; }; - }; - }; - firewallRuleForNode = mkOption { - default = {}; - description = '' - Allows you to set specific firewall rules just for traffic originating from another network node. - A corresponding rule `wg--node--to-` will be created to easily expose - services to that node. - ''; - type = types.attrsOf (types.submodule { - options = { - allowedTCPPorts = mkOption { - type = types.listOf types.port; - default = []; - description = "Convenience option to open specific TCP ports for traffic from another node."; + client = { + via = mkOption { + default = null; + type = types.nullOr types.str; + description = '' + The server node via which to connect to the network. + No client functionality will be activated if set to null. + ''; }; - allowedUDPPorts = mkOption { - type = types.listOf types.port; - default = []; - description = "Convenience option to open specific UDP ports for traffic from another node."; + + keepalive = mkOption { + default = true; + type = types.bool; + description = "Whether to keep this connection alive using PersistentKeepalive. Set to false only for networks where client and server IPs are stable."; }; }; - }); - }; - }; - })); + + priority = mkOption { + default = 40; + type = types.int; + description = "The order priority used when creating systemd netdev and network files."; + }; + + linkName = mkOption { + default = name; + type = types.str; + description = "The name for the created network interface."; + }; + + unitConfName = mkOption { + default = "${toString config.priority}-${config.linkName}"; + readOnly = true; + type = types.str; + description = '' + The name used for unit configuration files. This is a read-only option. + Access this if you want to add additional settings to the generated systemd units. + ''; + }; + + ipv4 = mkOption { + type = types.lazyOf types.net.ipv4; + default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv4Addresses.${nodeName}; + description = '' + The ipv4 address for this machine. If you do not set this explicitly, + a semi-stable ipv4 address will be derived automatically based on the + hostname of this machine. At least one participating server must reserve + a big-enough space of addresses by setting `reservedAddresses`. + See `net.cidr.assignIps` for more information on the algorithm. + ''; + }; + + ipv6 = mkOption { + type = types.lazyOf types.net.ipv6; + default = types.lazyValue (lib.wireguard.getNetwork inputs name).assignedIpv6Addresses.${nodeName}; + description = '' + The ipv6 address for this machine. If you do not set this explicitly, + a semi-stable ipv6 address will be derived automatically based on the + hostname of this machine. At least one participating server must reserve + a big-enough space of addresses by setting `reservedAddresses`. + See `net.cidr.assignIps` for more information on the algorithm. + ''; + }; + + addresses = mkOption { + type = types.listOf (types.lazyOf types.net.ip); + default = [ + (head options.ipv4.definitions) + (head options.ipv6.definitions) + ]; + description = '' + The ip addresses (v4 and/or v6) to use for this machine. + The actual network cidr will automatically be derived from all network participants. + By default this will just include {option}`ipv4` and {option}`ipv6` as configured. + ''; + }; + + firewallRuleForAll = mkOption { + default = {}; + description = '' + Allows you to set specific firewall rules for traffic originating from any participant in this + wireguard network. A corresponding rule `wg--to-` will be created to easily expose + services to the network. + ''; + type = types.submodule { + options = { + allowedTCPPorts = mkOption { + type = types.listOf types.port; + default = []; + description = "Convenience option to open specific TCP ports for traffic from the network."; + }; + allowedUDPPorts = mkOption { + type = types.listOf types.port; + default = []; + description = "Convenience option to open specific UDP ports for traffic from the network."; + }; + }; + }; + }; + + firewallRuleForNode = mkOption { + default = {}; + description = '' + Allows you to set specific firewall rules just for traffic originating from another network node. + A corresponding rule `wg--node--to-` will be created to easily expose + services to that node. + ''; + type = types.attrsOf ( + types.submodule { + options = { + allowedTCPPorts = mkOption { + type = types.listOf types.port; + default = []; + description = "Convenience option to open specific TCP ports for traffic from another node."; + }; + allowedUDPPorts = mkOption { + type = types.listOf types.port; + default = []; + description = "Convenience option to open specific UDP ports for traffic from another node."; + }; + }; + } + ); + }; + }; + } + ) + ); }; - config = mkIf (cfg != {}) (mergeToplevelConfigs - ["assertions" "age" "networking" "systemd"] - (mapAttrsToList configForNetwork cfg)); + config = mkIf (cfg != {}) ( + mergeToplevelConfigs ["assertions" "age" "networking" "systemd"] ( + mapAttrsToList configForNetwork cfg + ) + ); }