feat: allow reservation of addresses in wireguard network

This commit is contained in:
oddlama 2023-05-20 20:47:09 +02:00
parent 0221a24225
commit f95bc0eb30
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
4 changed files with 70 additions and 30 deletions

View file

@ -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 = {

View file

@ -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) {

View file

@ -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.
'';
};
};
}));
};

View file

@ -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