feat(wireguard): add ability to automatically assign addresses

This commit is contained in:
oddlama 2023-05-29 00:07:56 +02:00
parent 4e8103af47
commit c789e2de36
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
8 changed files with 136 additions and 47 deletions

10
flake.lock generated
View file

@ -210,11 +210,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1685019994, "lastModified": 1685189510,
"narHash": "sha256-81o6SKZPALvib21hIOMx2lIhFSs0mRy0PfPvg0zsfTk=", "narHash": "sha256-Hq5WF7zIixojPgvhgcd6MBvywwycVZ9wpK/8ogOyoaA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "d1f04b0f365a34896a37d9015637796537ec88a3", "rev": "2d963854ae2499193c0c72fd67435fee34d3e4fd",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -260,8 +260,8 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1685048145, "lastModified": 1685202716,
"narHash": "sha256-IGvX/JZReujsF8dLJUr0+DdjplR5qeSOWKQbpXc9j9E=", "narHash": "sha256-1+YC2fjzYI0XKmKIC0jswSYYWCPx2gOAf4/6FgcP+9Q=",
"type": "git", "type": "git",
"url": "file:///root/projects/microvm.nix" "url": "file:///root/projects/microvm.nix"
}, },

View file

@ -106,7 +106,7 @@
# Collect all defined microvm nodes from each colmena node # Collect all defined microvm nodes from each colmena node
microvmNodes = nixpkgs.lib.concatMapAttrs (_: node: microvmNodes = nixpkgs.lib.concatMapAttrs (_: node:
nixpkgs.lib.mapAttrs' 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 {})) (node.config.extra.microvms.vms or {}))
self.colmenaNodes; self.colmenaNodes;
# Expose all nodes in a single attribute # Expose all nodes in a single attribute

View file

@ -231,7 +231,7 @@
}; };
in in
assert lib.assertMsg (cidrSize >= 2 && cidrSize <= 62) 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) assert lib.assertMsg (nHosts <= capacity - nInit)
"assignIps: number of hosts (${toString nHosts}) must be <= capacity (${toString capacity}) - reserved (${toString 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 # Assign an ip in the subnet to each element, in order
@ -242,7 +242,9 @@
sortedHosts) sortedHosts)
.assigned; .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. # Checks whether the given address (with or without cidr notation) is an ipv6 address.
isv6 = lib.hasInfix ":"; isv6 = lib.hasInfix ":";
}; };

View file

@ -25,6 +25,13 @@ in {
boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" "r8169"]; 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 extra.microvms.vms = let
defineVm = id: { defineVm = id: {
inherit id; inherit id;

View file

@ -36,18 +36,24 @@
mergeToplevelConfigs mergeToplevelConfigs
; ;
inherit
(extraLib.types)
lazyOf
lazyValue
;
inherit (config.lib) net; inherit (config.lib) net;
cfg = config.extra.wireguard; cfg = config.extra.wireguard;
configForNetwork = wgName: wgCfg: let configForNetwork = wgName: wgCfg: let
inherit inherit
(extraLib.wireguard wgName) (extraLib.wireguard wgName)
associatedClientNodes
associatedNodes
associatedServerNodes
externalPeerName externalPeerName
externalPeerNamesRaw externalPeerNamesRaw
networkCidrs networkCidrs
participatingClientNodes
participatingNodes
participatingServerNodes
peerPresharedKeyPath peerPresharedKeyPath
peerPresharedKeySecret peerPresharedKeySecret
peerPrivateKeyPath peerPrivateKeyPath
@ -65,14 +71,14 @@
# All nodes that use our node as the via into the wireguard network # All nodes that use our node as the via into the wireguard network
ourClientNodes = ourClientNodes =
optionals isServer 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. # The list of peers for which we have to know the psk.
neededPeers = neededPeers =
if isServer if isServer
then then
# Other servers in the same network # Other servers in the same network
filterSelf associatedServerNodes filterSelf participatingServerNodes
# Our external peers # Our external peers
++ map externalPeerName (attrNames wgCfg.server.externalPeers) ++ map externalPeerName (attrNames wgCfg.server.externalPeers)
# Our clients # Our clients
@ -102,12 +108,12 @@
# plus traffic for any of its external peers # plus traffic for any of its external peers
++ attrValues snCfg.server.externalPeers ++ attrValues snCfg.server.externalPeers
# plus traffic for any client that is connected via that server # 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 { in {
assertions = [ 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."; 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}"; Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
}; };
}) })
(filterSelf associatedServerNodes) (filterSelf participatingServerNodes)
# All our external peers # All our external peers
++ mapAttrsToList (extPeer: ips: let ++ mapAttrsToList (extPeer: ips: let
peerName = externalPeerName extPeer; peerName = externalPeerName extPeer;
@ -241,6 +247,7 @@ in {
type = types.lazyAttrsOf (types.submodule ({ type = types.lazyAttrsOf (types.submodule ({
config, config,
name, name,
options,
... ...
}: { }: {
options = { options = {
@ -340,8 +347,8 @@ in {
}; };
ipv4 = mkOption { ipv4 = mkOption {
type = net.types.ipv4; type = lazyOf net.types.ipv4;
default = spannedReservedNetwork.cidrv4; default = lazyValue (extraLib.wireguard name).assignedIpv4Addresses.${nodeName};
description = mdDoc '' description = mdDoc ''
The ipv4 address for this machine. If you do not set this explicitly, 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 a semi-stable ipv4 address will be derived automatically based on the
@ -352,8 +359,8 @@ in {
}; };
ipv6 = mkOption { ipv6 = mkOption {
type = net.types.ipv6; type = lazyOf net.types.ipv6;
default = ; default = lazyValue (extraLib.wireguard name).assignedIpv6Addresses.${nodeName};
description = mdDoc '' description = mdDoc ''
The ipv6 address for this machine. If you do not set this explicitly, 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 a semi-stable ipv6 address will be derived automatically based on the
@ -364,8 +371,11 @@ in {
}; };
addresses = mkOption { addresses = mkOption {
type = types.listOf net.types.ip; type = types.listOf (lazyOf net.types.ip);
default = [config.ipv4 config.ipv6]; default = [
(head options.ipv4.definitions)
(head options.ipv6.definitions)
];
description = mdDoc '' description = mdDoc ''
The ip addresses (v4 and/or v6) to use for this machine. The ip addresses (v4 and/or v6) to use for this machine.
The actual network cidr will automatically be derived from all network participants. 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 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 # - 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 # - force nodes to opt-in or allow nodes to opt-out? sometimes a node want's

View file

@ -26,10 +26,10 @@
inherit inherit
(self.extraLib.wireguard wgName) (self.extraLib.wireguard wgName)
allPeers allPeers
associatedNodes
associatedServerNodes
associatedClientNodes
externalPeersForNode externalPeersForNode
participatingClientNodes
participatingNodes
participatingServerNodes
peerPresharedKeyFile peerPresharedKeyFile
peerPrivateKeyFile peerPrivateKeyFile
peerPublicKeyFile peerPublicKeyFile
@ -76,11 +76,11 @@
["echo ==== ${wgName} ===="] ["echo ==== ${wgName} ===="]
++ map generatePeerKeys (attrNames allPeers) ++ map generatePeerKeys (attrNames allPeers)
# All server-nodes need a psk for each other, but not reflexive. # 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 # 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 # Each server-node need a psk for all their external peers
++ psksForPeerCombinations associatedServerNodes (n: attrNames (externalPeersForNode n)); ++ psksForPeerCombinations participatingServerNodes (n: attrNames (externalPeersForNode n));
in in
pkgs.writeShellScript "generate-wireguard-keys" '' pkgs.writeShellScript "generate-wireguard-keys" ''
set -euo pipefail set -euo pipefail

View file

@ -20,7 +20,7 @@
map map
(peer: {inherit wgName serverNode peer;}) (peer: {inherit wgName serverNode peer;})
(attrNames self.nodes.${serverNode}.config.extra.wireguard.${wgName}.server.externalPeers)) (attrNames self.nodes.${serverNode}.config.extra.wireguard.${wgName}.server.externalPeers))
(self.extraLib.wireguard wgName).associatedServerNodes; (self.extraLib.wireguard wgName).participatingServerNodes;
allExternalPeers = concatMap externalPeersForNet wireguardNetworks; allExternalPeers = concatMap externalPeersForNet wireguardNetworks;
in in
pkgs.writeShellScript "show-wireguard-qr" '' pkgs.writeShellScript "show-wireguard-qr" ''

View file

@ -5,9 +5,11 @@
}: let }: let
inherit inherit
(nixpkgs.lib) (nixpkgs.lib)
all
assertMsg assertMsg
attrNames attrNames
attrValues attrValues
concatLists
concatMap concatMap
concatMapStrings concatMapStrings
concatStringsSep concatStringsSep
@ -15,24 +17,59 @@
escapeShellArg escapeShellArg
filter filter
flatten flatten
flip
foldAttrs foldAttrs
foldl' foldl'
genAttrs genAttrs
genList genList
head head
isAttrs
mapAttrs' mapAttrs'
mergeAttrs mergeAttrs
mkMerge mkMerge
mkOptionType
nameValuePair nameValuePair
optionalAttrs optionalAttrs
partition partition
recursiveUpdate recursiveUpdate
removeSuffix removeSuffix
showOption
stringToCharacters stringToCharacters
substring substring
unique unique
; ;
in rec { 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 = <actual value>; }.
# 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 # Counts how often each element occurrs in xs
countOccurrences = let countOccurrences = let
addOrUpdate = acc: x: addOrUpdate = acc: x:
@ -169,11 +206,12 @@ in rec {
rageDecryptArgs = "${rageMasterIdentityArgs}"; rageDecryptArgs = "${rageMasterIdentityArgs}";
rageEncryptArgs = "${rageMasterIdentityArgs} ${rageExtraEncryptionPubkeys}"; 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 related functions that are reused in several files of this flake
wireguard = wgName: rec { 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. # 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 # Returns the given node's wireguard configuration of this network
wgCfgOf = node: self.nodes.${node}.config.extra.wireguard.${wgName}; wgCfgOf = node: self.nodes.${node}.config.extra.wireguard.${wgName};
@ -205,22 +243,22 @@ in rec {
in "wireguard-${wgName}-psks-${peer1}+${peer2}"; in "wireguard-${wgName}-psks-${peer1}+${peer2}";
# All nodes that are part of this network # All nodes that are part of this network
associatedNodes = participatingNodes =
filter filter
(n: builtins.hasAttr wgName self.nodes.${n}.config.extra.wireguard) (n: builtins.hasAttr wgName self.nodes.${n}.config.extra.wireguard)
(attrNames self.nodes); (attrNames self.nodes);
# Partition nodes by whether they are servers # Partition nodes by whether they are servers
_associatedNodes_isServerPartition = _participatingNodes_isServerPartition =
partition partition
(n: (wgCfgOf n).server.host != null) (n: (wgCfgOf n).server.host != null)
associatedNodes; participatingNodes;
associatedServerNodes = _associatedNodes_isServerPartition.right; participatingServerNodes = _participatingNodes_isServerPartition.right;
associatedClientNodes = _associatedNodes_isServerPartition.wrong; participatingClientNodes = _participatingNodes_isServerPartition.wrong;
# Maps all nodes that are part of this network to their addresses # 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}"; externalPeerName = p: "external-${p}";
@ -231,34 +269,66 @@ in rec {
# All peers that are defined as externalPeers on any node. # All peers that are defined as externalPeers on any node.
# Prepends "external-" to their name. # Prepends "external-" to their name.
allExternalPeers = concatAttrs (map externalPeersForNode associatedNodes); allExternalPeers = concatAttrs (map externalPeersForNode participatingNodes);
# All peers that are part of this network # All peers that are part of this network
allPeers = nodePeers // allExternalPeers; allPeers = nodePeers // allExternalPeers;
# Concatenation of all external peer names names without any transformations. # 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. # A list of all occurring addresses.
usedAddresses = usedAddresses =
concatMap (n: (wgCfgOf n).addresses) associatedNodes concatMap (n: (wgCfgOf n).addresses) participatingNodes
++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) associatedNodes); ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes);
# The cidrv4 and cidrv6 of the network spanned by all reserved addresses only. # A list of all occurring addresses, but only includes addresses that
# Used to determine automatically assigned addresses first. # are not assigned automatically.
spannedReservedNetwork = explicitlyUsedAddresses =
net.cidr.merge (concatMap (n: (wgCfgOf n).server.reservedAddresses) associatedServerNodes); 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. # 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. # This also takes into account any reserved address ranges that should be part of the network.
networkAddresses = networkAddresses =
net.cidr.merge (usedAddresses 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 # The network spanning cidr addresses. The respective cidrv4 and cirdv6 are only
# included if they exist. # included if they exist.
networkCidrs = filter (x: x != null) (attrValues networkAddresses); 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, # Appends / replaces the correct cidr length to the argument,
# so that the resulting address is in the cidr. # so that the resulting address is in the cidr.
toNetworkAddr = addr: let toNetworkAddr = addr: let