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": {
"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"
},

View file

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

View file

@ -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 ":";
};

View file

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

View file

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

View file

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

View file

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

View file

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