From c789e2de365d26844a45a4b3ddce9ab00a331c92 Mon Sep 17 00:00:00 2001 From: oddlama Date: Mon, 29 May 2023 00:07:56 +0200 Subject: [PATCH] feat(wireguard): add ability to automatically assign addresses --- flake.lock | 10 +-- flake.nix | 2 +- hosts/common/core/system.nix | 6 +- hosts/ward/default.nix | 7 ++ modules/wireguard.nix | 40 +++++++---- nix/apps/generate-wireguard-keys.nix | 12 ++-- nix/apps/show-wireguard-qr.nix | 2 +- nix/lib.nix | 104 ++++++++++++++++++++++----- 8 files changed, 136 insertions(+), 47 deletions(-) diff --git a/flake.lock b/flake.lock index 4b37368..455bd0e 100644 --- a/flake.lock +++ b/flake.lock @@ -210,11 +210,11 @@ ] }, "locked": { - "lastModified": 1685019994, - "narHash": "sha256-81o6SKZPALvib21hIOMx2lIhFSs0mRy0PfPvg0zsfTk=", + "lastModified": 1685189510, + "narHash": "sha256-Hq5WF7zIixojPgvhgcd6MBvywwycVZ9wpK/8ogOyoaA=", "owner": "nix-community", "repo": "home-manager", - "rev": "d1f04b0f365a34896a37d9015637796537ec88a3", + "rev": "2d963854ae2499193c0c72fd67435fee34d3e4fd", "type": "github" }, "original": { @@ -260,8 +260,8 @@ ] }, "locked": { - "lastModified": 1685048145, - "narHash": "sha256-IGvX/JZReujsF8dLJUr0+DdjplR5qeSOWKQbpXc9j9E=", + "lastModified": 1685202716, + "narHash": "sha256-1+YC2fjzYI0XKmKIC0jswSYYWCPx2gOAf4/6FgcP+9Q=", "type": "git", "url": "file:///root/projects/microvm.nix" }, diff --git a/flake.nix b/flake.nix index 9def6fe..de879d8 100644 --- a/flake.nix +++ b/flake.nix @@ -106,7 +106,7 @@ # Collect all defined microvm nodes from each colmena node microvmNodes = nixpkgs.lib.concatMapAttrs (_: node: nixpkgs.lib.mapAttrs' - (vm: def: nixpkgs.lib.nameValuePair def.nodeName node.config.microvm.vms.${vm}) + (vm: def: nixpkgs.lib.nameValuePair def.nodeName node.config.microvm.vms.${vm}.config) (node.config.extra.microvms.vms or {})) self.colmenaNodes; # Expose all nodes in a single attribute diff --git a/hosts/common/core/system.nix b/hosts/common/core/system.nix index c8e5ebe..0149f93 100644 --- a/hosts/common/core/system.nix +++ b/hosts/common/core/system.nix @@ -231,7 +231,7 @@ }; in assert lib.assertMsg (cidrSize >= 2 && cidrSize <= 62) - "assignIps: cidrSize=${cidrSize} is not in [2, 62]."; + "assignIps: cidrSize=${toString cidrSize} is not in [2, 62]."; assert lib.assertMsg (nHosts <= capacity - nInit) "assignIps: number of hosts (${toString nHosts}) must be <= capacity (${toString capacity}) - reserved (${toString nInit})"; # Assign an ip in the subnet to each element, in order @@ -242,7 +242,9 @@ sortedHosts) .assigned; }; - ip = { + ip = rec { + # Checks whether the given address (with or without cidr notation) is an ipv4 address. + isv4 = x: !isv6 x; # Checks whether the given address (with or without cidr notation) is an ipv6 address. isv6 = lib.hasInfix ":"; }; diff --git a/hosts/ward/default.nix b/hosts/ward/default.nix index 4bad082..a0c5c48 100644 --- a/hosts/ward/default.nix +++ b/hosts/ward/default.nix @@ -25,6 +25,13 @@ in { boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" "r8169"]; + extra.wireguard.proxy-sentinel.server = { + host = "TODO REMOVE"; + port = 51443; + reservedAddresses = ["10.0.43.0/24" "fd43::/120"]; + openFirewallRules = ["untrusted-to-local"]; + }; + extra.microvms.vms = let defineVm = id: { inherit id; diff --git a/modules/wireguard.nix b/modules/wireguard.nix index cdb335d..dcbabb9 100644 --- a/modules/wireguard.nix +++ b/modules/wireguard.nix @@ -36,18 +36,24 @@ mergeToplevelConfigs ; + inherit + (extraLib.types) + lazyOf + lazyValue + ; + inherit (config.lib) net; cfg = config.extra.wireguard; configForNetwork = wgName: wgCfg: let inherit (extraLib.wireguard wgName) - associatedClientNodes - associatedNodes - associatedServerNodes externalPeerName externalPeerNamesRaw networkCidrs + participatingClientNodes + participatingNodes + participatingServerNodes peerPresharedKeyPath peerPresharedKeySecret peerPrivateKeyPath @@ -65,14 +71,14 @@ # All nodes that use our node as the via into the wireguard network ourClientNodes = optionals isServer - (filter (n: (wgCfgOf n).client.via == nodeName) associatedClientNodes); + (filter (n: (wgCfgOf n).client.via == nodeName) participatingClientNodes); # The list of peers for which we have to know the psk. neededPeers = if isServer then # Other servers in the same network - filterSelf associatedServerNodes + filterSelf participatingServerNodes # Our external peers ++ map externalPeerName (attrNames wgCfg.server.externalPeers) # Our clients @@ -102,12 +108,12 @@ # plus traffic for any of its external peers ++ attrValues snCfg.server.externalPeers # plus traffic for any client that is connected via that server - ++ map (n: (wgCfgOf n).addresses) (filter (n: (wgCfgOf n).client.via == serverNode) associatedClientNodes) + ++ map (n: (wgCfgOf n).addresses) (filter (n: (wgCfgOf n).client.via == serverNode) participatingClientNodes) ); in { assertions = [ { - assertion = any (n: (wgCfgOf n).server.host != null) associatedNodes; + assertion = any (n: (wgCfgOf n).server.host != null) participatingNodes; message = "${assertionPrefix}: At least one node in a network must be a server."; } { @@ -184,7 +190,7 @@ Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}"; }; }) - (filterSelf associatedServerNodes) + (filterSelf participatingServerNodes) # All our external peers ++ mapAttrsToList (extPeer: ips: let peerName = externalPeerName extPeer; @@ -241,6 +247,7 @@ in { type = types.lazyAttrsOf (types.submodule ({ config, name, + options, ... }: { options = { @@ -340,8 +347,8 @@ in { }; ipv4 = mkOption { - type = net.types.ipv4; - default = spannedReservedNetwork.cidrv4; + type = lazyOf net.types.ipv4; + default = lazyValue (extraLib.wireguard name).assignedIpv4Addresses.${nodeName}; description = mdDoc '' 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 @@ -352,8 +359,8 @@ in { }; ipv6 = mkOption { - type = net.types.ipv6; - default = ; + type = lazyOf net.types.ipv6; + default = lazyValue (extraLib.wireguard name).assignedIpv6Addresses.${nodeName}; description = mdDoc '' 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 @@ -364,8 +371,11 @@ in { }; addresses = mkOption { - type = types.listOf net.types.ip; - default = [config.ipv4 config.ipv6]; + type = types.listOf (lazyOf net.types.ip); + default = [ + (head options.ipv4.definitions) + (head options.ipv6.definitions) + ]; description = mdDoc '' The ip addresses (v4 and/or v6) to use for this machine. The actual network cidr will automatically be derived from all network participants. @@ -373,7 +383,7 @@ in { ''; }; - # TODO this needs to be implemented. + # TODO this is not yet implemented. # - is 0.0.0.0/0 also for valid for routing global ipv6? # - is 0.0.0.0/0 routing private spaces such as 192.168.1 ? that'd be baaad # - force nodes to opt-in or allow nodes to opt-out? sometimes a node want's diff --git a/nix/apps/generate-wireguard-keys.nix b/nix/apps/generate-wireguard-keys.nix index 6650138..f231764 100644 --- a/nix/apps/generate-wireguard-keys.nix +++ b/nix/apps/generate-wireguard-keys.nix @@ -26,10 +26,10 @@ inherit (self.extraLib.wireguard wgName) allPeers - associatedNodes - associatedServerNodes - associatedClientNodes externalPeersForNode + participatingClientNodes + participatingNodes + participatingServerNodes peerPresharedKeyFile peerPrivateKeyFile peerPublicKeyFile @@ -76,11 +76,11 @@ ["echo ==== ${wgName} ===="] ++ map generatePeerKeys (attrNames allPeers) # All server-nodes need a psk for each other, but not reflexive. - ++ psksForPeerCombinations associatedServerNodes (n: filter (x: x != n) associatedServerNodes) + ++ psksForPeerCombinations participatingServerNodes (n: filter (x: x != n) participatingServerNodes) # Each server-node need a psk for all client nodes - ++ psksForPeerCombinations associatedServerNodes (_: associatedClientNodes) + ++ psksForPeerCombinations participatingServerNodes (_: participatingClientNodes) # Each server-node need a psk for all their external peers - ++ psksForPeerCombinations associatedServerNodes (n: attrNames (externalPeersForNode n)); + ++ psksForPeerCombinations participatingServerNodes (n: attrNames (externalPeersForNode n)); in pkgs.writeShellScript "generate-wireguard-keys" '' set -euo pipefail diff --git a/nix/apps/show-wireguard-qr.nix b/nix/apps/show-wireguard-qr.nix index 4468164..1bb67ff 100644 --- a/nix/apps/show-wireguard-qr.nix +++ b/nix/apps/show-wireguard-qr.nix @@ -20,7 +20,7 @@ map (peer: {inherit wgName serverNode peer;}) (attrNames self.nodes.${serverNode}.config.extra.wireguard.${wgName}.server.externalPeers)) - (self.extraLib.wireguard wgName).associatedServerNodes; + (self.extraLib.wireguard wgName).participatingServerNodes; allExternalPeers = concatMap externalPeersForNet wireguardNetworks; in pkgs.writeShellScript "show-wireguard-qr" '' diff --git a/nix/lib.nix b/nix/lib.nix index f9093b7..8a205ba 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -5,9 +5,11 @@ }: let inherit (nixpkgs.lib) + all assertMsg attrNames attrValues + concatLists concatMap concatMapStrings concatStringsSep @@ -15,24 +17,59 @@ escapeShellArg filter flatten + flip foldAttrs foldl' genAttrs genList head + isAttrs mapAttrs' mergeAttrs mkMerge + mkOptionType nameValuePair optionalAttrs partition recursiveUpdate removeSuffix + showOption stringToCharacters substring unique ; in rec { + types = rec { + # Checks whether the value is a lazy value without causing + # it's value to be evaluated + isLazyValue = x: isAttrs x && x ? _lazyValue; + # Constructs a lazy value holding the given value. + lazyValue = value: {_lazyValue = value;}; + + # Represents a lazy value of the given type, which + # holds the actual value as an attrset like { _lazyValue = ; }. + # This allows the option to be defined and filtered from a defintion + # list without evaluating the value. + lazyValueOf = type: + mkOptionType rec { + name = "lazyValueOf ${type.name}"; + inherit (type) description descriptionClass emptyValue getSubOptions getSubModules; + check = isLazyValue; + merge = loc: defs: + assert assertMsg + (all (x: type.check x._lazyValue) defs) + "The option `${showOption loc}` is defined with a lazy value holding an invalid type"; + nixpkgs.lib.types.mergeOneOption loc defs; + substSubModules = m: nixpkgs.lib.types.uniq (type.substSubModules m); + functor = (nixpkgs.lib.types.defaultFunctor name) // {wrapped = type;}; + nestedTypes.elemType = type; + }; + + # Represents a value or lazy value of the given type that will + # automatically be coerced to the given type when merged. + lazyOf = type: nixpkgs.lib.types.coercedTo (lazyValueOf type) (x: x._lazyValue) type; + }; + # Counts how often each element occurrs in xs countOccurrences = let addOrUpdate = acc: x: @@ -169,11 +206,12 @@ in rec { rageDecryptArgs = "${rageMasterIdentityArgs}"; rageEncryptArgs = "${rageMasterIdentityArgs} ${rageExtraEncryptionPubkeys}"; + # TODO merge this into a _meta readonly option in the wireguard module # Wireguard related functions that are reused in several files of this flake wireguard = wgName: rec { - # Get access to the networking lib by referring to one of the associated nodes. + # Get access to the networking lib by referring to one of the participating nodes. # Not ideal, but ok. - inherit (self.nodes.${head associatedNodes}.config.lib) net; + inherit (self.nodes.${head participatingNodes}.config.lib) net; # Returns the given node's wireguard configuration of this network wgCfgOf = node: self.nodes.${node}.config.extra.wireguard.${wgName}; @@ -205,22 +243,22 @@ in rec { in "wireguard-${wgName}-psks-${peer1}+${peer2}"; # All nodes that are part of this network - associatedNodes = + participatingNodes = filter (n: builtins.hasAttr wgName self.nodes.${n}.config.extra.wireguard) (attrNames self.nodes); # Partition nodes by whether they are servers - _associatedNodes_isServerPartition = + _participatingNodes_isServerPartition = partition (n: (wgCfgOf n).server.host != null) - associatedNodes; + participatingNodes; - associatedServerNodes = _associatedNodes_isServerPartition.right; - associatedClientNodes = _associatedNodes_isServerPartition.wrong; + participatingServerNodes = _participatingNodes_isServerPartition.right; + participatingClientNodes = _participatingNodes_isServerPartition.wrong; # Maps all nodes that are part of this network to their addresses - nodePeers = genAttrs associatedNodes (n: (wgCfgOf n).addresses); + nodePeers = genAttrs participatingNodes (n: (wgCfgOf n).addresses); externalPeerName = p: "external-${p}"; @@ -231,34 +269,66 @@ in rec { # All peers that are defined as externalPeers on any node. # Prepends "external-" to their name. - allExternalPeers = concatAttrs (map externalPeersForNode associatedNodes); + 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) associatedNodes; + externalPeerNamesRaw = concatMap (n: attrNames (wgCfgOf n).server.externalPeers) participatingNodes; # A list of all occurring addresses. usedAddresses = - concatMap (n: (wgCfgOf n).addresses) associatedNodes - ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) associatedNodes); + concatMap (n: (wgCfgOf n).addresses) participatingNodes + ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); - # 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) associatedServerNodes); + # 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 + (self.nodes.${n}.options.extra.wireguard.type.functor.wrapped.getSubOptions (wgCfgOf n)).addresses.definitions)) + ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); # 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) associatedServerNodes); + ++ concatMap (n: (wgCfgOf n).server.reservedAddresses) participatingServerNodes); # The network spanning cidr addresses. The respective cidrv4 and cirdv6 are only # included if they exist. networkCidrs = filter (x: x != null) (attrValues networkAddresses); + # 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); + + # Assigns an ipv4 address from spannedReservedNetwork.cidrv4 + # to each participant that has not explicitly specified an ipv4 address. + 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 + # 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)) + participatingNodes; + + # Assigns an ipv4 address from spannedReservedNetwork.cidrv4 + # to each participant that has not explicitly specified an ipv4 address. + assignedIpv6Addresses = assert assertMsg + (spannedReservedNetwork.cidrv6 != null) + "Wireguard network '${wgName}': At least one participating node must reserve a cidrv6 address via `reservedAddresses` so that ipv4 addresses can be assigned automatically from that network."; + 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)) + participatingNodes; + # Appends / replaces the correct cidr length to the argument, # so that the resulting address is in the cidr. toNetworkAddr = addr: let