diff --git a/hosts/common/core/system.nix b/hosts/common/core/system.nix index 14bb5b6..b5fd08a 100644 --- a/hosts/common/core/system.nix +++ b/hosts/common/core/system.nix @@ -67,16 +67,16 @@ # > net.cidr.canonicalize "192.168.1.100/24" # "192.168.1.0/24" canonicalize = x: libWithNet.net.cidr.make (libWithNet.net.cidr.length x) (ip x); - # coercev4 :: [cidr4 | ipv4] -> (cidr4 | null) + # mergev4 :: [cidr4 | ipv4] -> (cidr4 | null) # - # Returns the smallest cidr network that includes all given addresses. + # Returns the smallest cidr network that includes all given networks. # If no cidr mask is given, /32 is assumed. # # Examples: # - # > net.cidr.coercev4 ["192.168.1.1/24" "192.168.6.1/32"] + # > net.cidr.mergev4 ["192.168.1.1/24" "192.168.6.1/32"] # "192.168.0.0/21" - coercev4 = addrs_: let + mergev4 = addrs_: let # Append /32 if necessary addrs = map (x: if lib.hasInfix "/" x @@ -104,20 +104,20 @@ addrs) possibleLengths); in - assert lib.assertMsg (!lib.any (lib.hasInfix ":") addrs) "coercev4 cannot operate on ipv6 addresses"; + assert lib.assertMsg (!lib.any (lib.hasInfix ":") addrs) "mergev4 cannot operate on ipv6 addresses"; if addrs == [] then null else libWithNet.net.cidr.make bestLength firstIp; - # coercev6 :: [cidr6 | ipv6] -> (cidr6 | null) + # mergev6 :: [cidr6 | ipv6] -> (cidr6 | null) # - # Returns the smallest cidr network that includes all given addresses. + # Returns the smallest cidr network that includes all given networks. # If no cidr mask is given, /128 is assumed. # # Examples: # - # > net.cidr.coercev6 ["fd00:dead:cafe::/64" "fd00:fd12:3456:7890::/56"] + # > net.cidr.mergev6 ["fd00:dead:cafe::/64" "fd00:fd12:3456:7890::/56"] # "fd00:c000::/18" - coercev6 = addrs_: let + mergev6 = addrs_: let # Append /128 if necessary addrs = map (x: if lib.hasInfix "/" x @@ -145,20 +145,20 @@ addrs) possibleLengths); in - assert lib.assertMsg (lib.all (lib.hasInfix ":") addrs) "coercev6 cannot operate on ipv4 addresses"; + assert lib.assertMsg (lib.all (lib.hasInfix ":") addrs) "mergev6 cannot operate on ipv4 addresses"; if addrs == [] then null else libWithNet.net.cidr.make bestLength firstIp; - # coerce :: [cidr] -> { cidrv4 = (cidr4 | null); cidrv6 = (cidr4 | null); } + # merge :: [cidr] -> { cidrv4 = (cidr4 | null); cidrv6 = (cidr4 | null); } # - # Returns the smallest cidr network that includes all given addresses, + # Returns the smallest cidr network that includes all given networks, # but yields two separate result for all given ipv4 and ipv6 addresses. - # Equivalent to calling coercev4 and coercev6 on a partition individually. - coerce = addrs: let + # Equivalent to calling mergev4 and mergev6 on a partition individually. + merge = addrs: let v4_and_v6 = lib.partition (lib.hasInfix ":") addrs; in { - cidrv4 = coercev4 v4_and_v6.wrong; - cidrv6 = coercev6 v4_and_v6.right; + cidrv4 = mergev4 v4_and_v6.wrong; + cidrv6 = mergev6 v4_and_v6.right; }; }; ip = { diff --git a/modules/microvms.nix b/modules/microvms.nix index 37fee8e..fc93bd0 100644 --- a/modules/microvms.nix +++ b/modules/microvms.nix @@ -187,6 +187,7 @@ inherit (vmCfg.networking) host; inherit (cfg.networking.wireguard) port; openFirewallRules = ["${vmCfg.networking.mainLinkName}-to-local"]; + reservedAddresses = [cfg.networking.wireguard.cidrv4 cfg.networking.wireguard.cidrv6]; }; # If We don't have such guarantees, so we must use a client-server architecture. client = optionalAttrs (cfg.networking.host == null) { diff --git a/modules/wireguard.nix b/modules/wireguard.nix index 315515f..8a5669c 100644 --- a/modules/wireguard.nix +++ b/modules/wireguard.nix @@ -3,7 +3,6 @@ lib, extraLib, pkgs, - nodes, nodeName, ... }: let @@ -42,9 +41,9 @@ configForNetwork = wgName: wgCfg: let inherit (extraLib.wireguard wgName) + associatedClientNodes associatedNodes associatedServerNodes - associatedClientNodes externalPeerName externalPeerNamesRaw peerPresharedKeyPath @@ -52,15 +51,14 @@ peerPrivateKeyPath peerPrivateKeySecret peerPublicKeyPath - usedAddresses toNetworkAddr + usedAddresses + wgCfgOf ; isServer = wgCfg.server.host != null; isClient = wgCfg.client.via != null; - filterSelf = filter (x: x != nodeName); - wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName}; # All nodes that use our node as the via into the wireguard network ourClientNodes = @@ -82,7 +80,7 @@ # Figure out if there are duplicate peers or addresses so we can # make an assertion later. duplicatePeers = duplicates externalPeerNamesRaw; - duplicateAddrs = duplicates (map net.cidr.ip usedAddresses); + duplicateAddrs = duplicates usedAddresses; # Adds context information to the assertions for this network assertionPrefix = "Wireguard network '${wgName}' on '${nodeName}'"; @@ -281,6 +279,22 @@ in { this node to act as a server. ''; }; + + reservedAddresses = mkOption { + type = types.listOf net.types.cidr; + default = []; + example = ["10.0.0.1/24" "fd00:cafe::/64"]; + description = mdDoc '' + Allows defining extra cidr network ranges that shall be reserved for this machine + and its children (i.e. external peers or via clients). Reservation means that those + address spaces will be guaranteed to be included in the spanned network. + + 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 = { @@ -341,6 +355,25 @@ in { By default this will just include {option}`ipv4` and {option}`ipv6` as configured. ''; }; + + # TODO this needs to be 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 + # to use the network without routing additional stuff. + # - allow specifying the route metric. + routedAddresses = mkOption { + type = types.listOf net.types.cidr; + default = []; + example = ["0.0.0.0/0"]; + description = mdDoc '' + Additional networks that are accessible through this machine. This will allow + other participants of the network to access these networks through the tunnel. + + Make sure to configure a NAT on the created interface (or that the proper routes + are generated) to allow inter-network communication. + ''; + }; }; })); }; diff --git a/nix/lib.nix b/nix/lib.nix index 1b2f0c4..783f7ab 100644 --- a/nix/lib.nix +++ b/nix/lib.nix @@ -138,6 +138,9 @@ in rec { # Not ideal, but ok. inherit (self.nodes.${head associatedNodes}.config.lib) net; + # Returns the given node's wireguard configuration of this network + wgCfgOf = node: self.nodes.${node}.config.extra.wireguard.${wgName}; + sortedPeers = peerA: peerB: if peerA < peerB then { @@ -173,21 +176,21 @@ in rec { # Partition nodes by whether they are servers _associatedNodes_isServerPartition = partition - (n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.host != null) + (n: (wgCfgOf n).server.host != null) associatedNodes; associatedServerNodes = _associatedNodes_isServerPartition.right; associatedClientNodes = _associatedNodes_isServerPartition.wrong; # Maps all nodes that are part of this network to their addresses - nodePeers = genAttrs associatedNodes (n: self.nodes.${n}.config.extra.wireguard.${wgName}.addresses); + nodePeers = genAttrs associatedNodes (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)) self.nodes.${node}.config.extra.wireguard.${wgName}.server.externalPeers; + mapAttrs' (p: nameValuePair (externalPeerName p)) (wgCfgOf node).server.externalPeers; # All peers that are defined as externalPeers on any node. # Prepends "external-" to their name. @@ -197,15 +200,18 @@ in rec { allPeers = nodePeers // allExternalPeers; # Concatenation of all external peer names names without any transformations. - externalPeerNamesRaw = concatMap (n: attrNames self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers) associatedNodes; + externalPeerNamesRaw = concatMap (n: attrNames (wgCfgOf n).server.externalPeers) associatedNodes; # A list of all occurring addresses. usedAddresses = - concatMap (n: self.nodes.${n}.config.extra.wireguard.${wgName}.addresses) associatedNodes - ++ flatten (concatMap (n: map (net.cidr.make 128) (attrValues self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers)) associatedNodes); + concatMap (n: (wgCfgOf n).addresses) associatedNodes + ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) associatedNodes); # The cidrv4 and cidrv6 of the network spanned by all participating peer addresses. - networkAddresses = net.cidr.coerce usedAddresses; + # 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); # Appends / replaces the correct cidr length to the argument, # so that the resulting address is in the cidr. @@ -221,7 +227,7 @@ in rec { # storing them in the nix-store. wgQuickConfigScript = system: serverNode: extPeer: let pkgs = self.pkgs.${system}; - snCfg = self.nodes.${serverNode}.config.extra.wireguard.${wgName}; + snCfg = wgCfgOf serverNode; peerName = externalPeerName extPeer; addresses = map toNetworkAddr snCfg.server.externalPeers.${extPeer}; in