1
1
Fork 1
mirror of https://github.com/oddlama/nix-config.git synced 2025-10-10 23:00:39 +02:00

feat: implement cidr coersion to automatically determine wireguard network size from participants

This commit is contained in:
oddlama 2023-05-20 15:57:19 +02:00
parent 6d8f8ab2e3
commit 4057ee9051
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
14 changed files with 240 additions and 29 deletions

6
flake.lock generated
View file

@ -31,11 +31,11 @@
]
},
"locked": {
"lastModified": 1683715679,
"narHash": "sha256-Zq2liHoVTNYql94XPTpEInQq5yY0NjRa9ZLYJv55dgE=",
"lastModified": 1684539260,
"narHash": "sha256-lF3+vp2UZwBjzF4pnOKYZrQOCFdnOdtvGmaFIzsaMN4=",
"owner": "oddlama",
"repo": "agenix-rekey",
"rev": "e5e84230bfa071685a05acdc11a94e3be672e541",
"rev": "e9a2bad33b7b1634af65cbc809fc31776df41fe5",
"type": "github"
},
"original": {

View file

@ -45,6 +45,7 @@ in {
'';
};
# TODO mkForce nftables
nftables.firewall = {
zones = lib.mkForce {
local.localZone = true;

View file

@ -14,14 +14,142 @@
lib.recursiveUpdate libWithNet {
net = {
cidr = rec {
# host :: (ip | mac | integer) -> cidr -> ip
#
# Wrapper that extends the original host function to
# check whether the argument `n` is in-range for the given cidr.
#
# Examples:
#
# > net.cidr.host 255 "192.168.1.0/24"
# "192.168.1.255"
# > net.cidr.host (256) "192.168.1.0/24"
# <fails with an error message>
# > net.cidr.host (-1) "192.168.1.0/24"
# "192.168.1.255"
# > net.cidr.host (-256) "192.168.1.0/24"
# "192.168.1.0"
# > net.cidr.host (-257) "192.168.1.0/24"
# <fails with an error message>
host = i: n: let
cap = libWithNet.net.cidr.capacity n;
in
assert lib.assertMsg (i >= (-cap) && i < cap) "The host ${toString i} lies outside of ${n}";
libWithNet.net.cidr.host i n;
# hostCidr :: (ip | mac | integer) -> cidr -> cidr
#
# Returns the nth host in the given cidr range (like cidr.host)
# but as a cidr that retains the original prefix length.
#
# Examples:
#
# > net.cidr.hostCidr 2 "192.168.1.0/24"
# "192.168.1.2/24"
hostCidr = n: x: "${libWithNet.net.cidr.host n x}/${toString (libWithNet.net.cidr.length x)}";
# ip :: (cidr | ip) -> ip
#
# Returns just the ip part of the cidr.
#
# Examples:
#
# > net.cidr.ip "192.168.1.100/24"
# "192.168.1.100"
# > net.cidr.ip "192.168.1.100"
# "192.168.1.100"
ip = x: lib.head (lib.splitString "/" x);
# canonicalize :: cidr -> cidr
#
# Replaces the ip of the cidr with the canonical network address
# (first contained address in range)
#
# Examples:
#
# > 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] -> (cidr4 | null)
#
# Returns the smallest cidr network that includes all given addresses
#
# Examples:
#
# > net.cidr.coercev4 ["192.168.1.1/24" "192.168.6.1/32"]
# "192.168.0.0/21"
coercev4 = addrs: let
# The smallest occurring length is the first we need to start checking, since
# any greater cidr length represents a smaller address range which
# wouldn't contain all of the original addresses.
startLength = lib.foldl' lib.min 32 (map libWithNet.net.cidr.length addrs);
possibleLengths = lib.reverseList (lib.range 0 startLength);
# The first ip address will be "expanded" in cidr length until it covers all other
# used addresses.
firstIp = ip (lib.head addrs);
# Return the first (i.e. greatest length -> smallest prefix) cidr length
# in the list that covers all used addresses
bestLength = lib.head (lib.filter
# All given addresses must be contained by the generated address.
(len:
lib.all
(x:
libWithNet.net.cidr.contains
(ip x)
(libWithNet.net.cidr.make len firstIp))
addrs)
possibleLengths);
in
assert lib.assertMsg (!lib.any (lib.hasInfix ":") addrs) "coercev4 cannot operate on ipv6 addresses";
if addrs == []
then null
else libWithNet.net.cidr.make bestLength firstIp;
# coercev6 :: [cidr6] -> (cidr6 | null)
#
# Returns the smallest cidr network that includes all given addresses
#
# Examples:
#
# > net.cidr.coercev6 ["fd00:dead:cafe::/64" "fd00:fd12:3456:7890::/56"]
# "fd00:c000::/18"
coercev6 = addrs: let
# The smallest occurring length is the first we need to start checking, since
# any greater cidr length represents a smaller address range which
# wouldn't contain all of the original addresses.
startLength = lib.foldl' lib.min 128 (map libWithNet.net.cidr.length addrs);
possibleLengths = lib.reverseList (lib.range 0 startLength);
# The first ip address will be "expanded" in cidr length until it covers all other
# used addresses.
firstIp = ip (lib.head addrs);
# Return the first (i.e. greatest length -> smallest prefix) cidr length
# in the list that covers all used addresses
bestLength = lib.head (lib.filter
# All given addresses must be contained by the generated address.
(len:
lib.all
(x:
libWithNet.net.cidr.contains
(ip x)
(libWithNet.net.cidr.make len firstIp))
addrs)
possibleLengths);
in
assert lib.assertMsg (lib.all (lib.hasInfix ":") addrs) "coercev6 cannot operate on ipv4 addresses";
if addrs == []
then null
else libWithNet.net.cidr.make bestLength firstIp;
# coerce :: [cidr] -> { cidrv4 = (cidr4 | null); cidrv6 = (cidr4 | null); }
#
# Returns the smallest cidr network that includes all given addresses,
# 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
v4_and_v6 = lib.partition (lib.hasInfix ":") addrs;
in {
cidrv4 = coercev4 v4_and_v6.wrong;
cidrv6 = coercev6 v4_and_v6.right;
};
};
ip = {
# Checks whether the given address (with or without cidr notation) is an ipv6 address.
isv6 = lib.hasInfix ":";
};
mac = {
# Adds offset to the given base address and ensures the result is in
@ -55,11 +183,22 @@
boot = {
initrd.systemd.enable = true;
# Add "rd.systemd.unit=rescue.target" to debug initrd
kernelParams = ["log_buf_len=10M"];
tmp.useTmpfs = true;
};
# Just before switching, remove the agenix directory if it exists.
# This can happen when a secret is used in the initrd because it will
# then be copied to the initramfs under the same path. This materializes
# /run/agenix as a directory which will cause issues when the actual system tries
# to create a link called /run/agenix. Agenix should probably fail in this case,
# but doesn't and instead puts the generation link into the existing directory.
# TODO See https://github.com/ryantm/agenix/pull/187.
system.activationScripts.removeAgenixLink.text = "[[ -d /run/agenix ]] && rm -rf /run/agenix";
system.activationScripts.agenixInstall.deps = ["removeAgenixLink"];
# Disable sudo which is entierly unnecessary.
security.sudo.enable = false;

View file

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBXXjI6uB26xOF0DPy/QyLladoGIKfAtofyqPgIkCH/g

View file

@ -93,6 +93,7 @@ in {
};
};
# TODO mkForce nftables
networking.nftables.firewall = {
zones = lib.mkForce {
lan.interfaces = ["lan-self"];
@ -188,5 +189,6 @@ in {
baseCidrv4 = lanCidrv4;
baseCidrv6 = lanCidrv6;
};
wireguard.openFirewallRules = ["lan-to-local"];
};
}

View file

@ -30,6 +30,7 @@ in {
};
};
# TODO mkForce nftables
networking.nftables.firewall = {
zones = lib.mkForce {
lan.interfaces = ["lan1"];

View file

@ -142,8 +142,8 @@
static = {
matchConfig.Name = vmCfg.networking.mainLinkName;
address = [
vmCfg.networking.static.ipv4
vmCfg.networking.static.ipv6
"${vmCfg.networking.static.ipv4}/${toString (net.cidr.length cfg.networking.static.baseCidrv4)}"
"${vmCfg.networking.static.ipv6}/${toString (net.cidr.length cfg.networking.static.baseCidrv6)}"
];
gateway = [
cfg.networking.host
@ -161,13 +161,14 @@
boot.initrd.systemd.enable = mkForce false;
# Create a firewall zone for the bridged traffic and secure vm traffic
# TODO mkForce nftables
networking.nftables.firewall = {
zones = lib.mkForce {
zones = mkForce {
"${vmCfg.networking.mainLinkName}".interfaces = [vmCfg.networking.mainLinkName];
"local-vms".interfaces = ["wg-local-vms"];
};
rules = lib.mkForce {
rules = mkForce {
"${vmCfg.networking.mainLinkName}-to-local" = {
from = [vmCfg.networking.mainLinkName];
to = ["local"];
@ -184,8 +185,8 @@
# We have a resolvable hostname / static ip, so all peers can directly communicate with us
server = optionalAttrs (cfg.networking.host != null) {
inherit (vmCfg.networking) host;
port = 51829;
openFirewallInRules = ["${vmCfg.networking.mainLinkName}-to-local"];
inherit (cfg.networking.wireguard) port;
openFirewallRules = ["${vmCfg.networking.mainLinkName}-to-local"];
};
# If We don't have such guarantees, so we must use a client-server architecture.
client = optionalAttrs (cfg.networking.host == null) {
@ -262,6 +263,18 @@ in {
description = mdDoc "The ipv6 network address range to use for internal vm traffic.";
default = "fddd::/64";
};
port = mkOption {
default = 51829;
type = types.port;
description = mdDoc "The port to listen on.";
};
openFirewallRules = mkOption {
default = [];
type = types.listOf types.str;
description = mdDoc "The {option}`port` will be opened for all of the given rules in the nftable-firewall.";
};
};
};
@ -387,8 +400,7 @@ in {
extra.wireguard."${nodeName}-local-vms" = {
server = {
inherit (cfg.networking) host;
port = 51829;
openFirewallInRules = ["lan-to-local"];
inherit (cfg.networking.wireguard) openFirewallRules port;
};
cidrv4 = net.cidr.hostCidr 1 cfg.networking.wireguard.cidrv4;
cidrv6 = net.cidr.hostCidr 1 cfg.networking.wireguard.cidrv6;

View file

@ -26,7 +26,6 @@
mkOption
optionalAttrs
optionals
splitString
types
;
@ -54,6 +53,7 @@
peerPrivateKeySecret
peerPublicKeyPath
usedAddresses
toNetworkAddr
;
isServer = wgCfg.server.host != null;
@ -82,7 +82,7 @@
# Figure out if there are duplicate peers or addresses so we can
# make an assertion later.
duplicatePeers = duplicates externalPeerNamesRaw;
duplicateAddrs = duplicates (map (x: head (splitString "/" x)) usedAddresses);
duplicateAddrs = duplicates (map net.cidr.ip usedAddresses);
# Adds context information to the assertions for this network
assertionPrefix = "Wireguard network '${wgName}' on '${nodeName}'";
@ -118,16 +118,27 @@
(isServer && wgCfg.server.openFirewall)
[wgCfg.server.port];
# TODO mkForce nftables
networking.nftables.firewall.rules =
mkIf
(isServer && wgCfg.server.openFirewallInRules != [])
(genAttrs wgCfg.server.openFirewallInRules (_: {allowedUDPPorts = [wgCfg.server.port];}));
(isServer && wgCfg.server.openFirewallRules != [])
(lib.mkForce (genAttrs wgCfg.server.openFirewallRules (_: {allowedUDPPorts = [wgCfg.server.port];})));
rekey.secrets =
concatAttrs (map
(other: {${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other;})
(other: {
${peerPresharedKeySecret nodeName other} = {
file = peerPresharedKeyPath nodeName other;
owner = "systemd-network";
};
})
neededPeers)
// {${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName;};
// {
${peerPrivateKeySecret nodeName} = {
file = peerPrivateKeyPath nodeName;
owner = "systemd-network";
};
};
systemd.network.netdevs."${toString wgCfg.priority}-${wgName}" = {
netdevConfig = {
@ -156,21 +167,18 @@
# plus each external peer's addresses,
# plus each client's addresses that is connected via that node.
AllowedIPs = snCfg.addresses;
# TODO this needed? or even wanted at all?
# ++ attrValues snCfg.server.externalPeers;
# ++ map (n: (wgCfgOf n).addresses) snCfg.ourClientNodes;
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
};
})
(filterSelf associatedServerNodes)
# All our external peers
++ mapAttrsToList (extPeer: allowedIPs: let
++ mapAttrsToList (extPeer: ips: let
peerName = externalPeerName extPeer;
in {
wireguardPeerConfig = {
PublicKey = builtins.readFile (peerPublicKeyPath peerName);
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path;
AllowedIPs = allowedIPs;
AllowedIPs = map (net.cidr.make 128) ips;
# Connections to external peers should always be kept alive
PersistentKeepalive = 25;
};
@ -207,7 +215,7 @@
systemd.network.networks."${toString wgCfg.priority}-${wgName}" = {
matchConfig.Name = wgName;
networkConfig.Address = wgCfg.addresses;
address = map toNetworkAddr wgCfg.addresses;
};
};
in {
@ -239,16 +247,16 @@ in {
description = mdDoc "Whether to open the firewall for the specified {option}`port`.";
};
openFirewallInRules = mkOption {
openFirewallRules = mkOption {
default = [];
type = types.listOf types.str;
description = mdDoc "The {option}`port` will be opened for all of the given rules in the nftable-firewall.";
};
externalPeers = mkOption {
type = types.attrsOf (types.listOf (net.types.cidr-in config.addresses));
type = types.attrsOf (types.listOf (net.types.ip-in config.addresses));
default = {};
example = {my-android-phone = ["10.0.0.97/32"];};
example = {my-android-phone = ["10.0.0.97"];};
description = mdDoc ''
Allows defining an extra set of peers that should be added to this wireguard network,
but will not be managed by this flake. (e.g. phones)
@ -329,6 +337,7 @@ in {
description = mdDoc ''
The addresses (with cidr mask) to configure for this interface.
The cidr mask determines this peers allowed address range as configured on other peers.
The actual network cidr will automatically be derived from all network participants.
By default this will just include {option}`cidrv4` and {option}`cidrv6` as configured.
'';
};

View file

@ -25,7 +25,6 @@
partition
recursiveUpdate
removeSuffix
splitString
substring
unique
;
@ -135,6 +134,10 @@ in rec {
# 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.
# Not ideal, but ok.
inherit (self.nodes.${head associatedNodes}.config.lib) net;
sortedPeers = peerA: peerB:
if peerA < peerB
then {
@ -199,7 +202,19 @@ in rec {
# A list of all occurring addresses.
usedAddresses =
concatMap (n: self.nodes.${n}.config.extra.wireguard.${wgName}.addresses) associatedNodes
++ flatten (concatMap (n: attrValues self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers) associatedNodes);
++ flatten (concatMap (n: map (net.cidr.make 128) (attrValues self.nodes.${n}.config.extra.wireguard.${wgName}.server.externalPeers)) associatedNodes);
# The cidrv4 and cidrv6 of the network spanned by all participating peer addresses.
networkAddresses = net.cidr.coerce usedAddresses;
# Appends / replaces the correct cidr length to the argument,
# so that the resulting address is in the cidr.
toNetworkAddr = addr: let
relevantNetworkAddr =
if net.ip.isv6 addr
then networkAddresses.cidrv6
else networkAddresses.cidrv4;
in "${net.cidr.ip addr}/${toString (net.cidr.length relevantNetworkAddr)}";
# Creates a script that when executed outputs a wg-quick compatible configuration
# file for use with external peers. This is a script so we can access secrets without
@ -208,6 +223,7 @@ in rec {
pkgs = self.pkgs.${system};
snCfg = self.nodes.${serverNode}.config.extra.wireguard.${wgName};
peerName = externalPeerName extPeer;
addresses = map toNetworkAddr snCfg.server.externalPeers.${extPeer};
in
pkgs.writeShellScript "create-wg-conf-${wgName}-${serverNode}-${extPeer}" ''
privKey=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPrivateKeyPath peerName)}) \
@ -217,7 +233,7 @@ in rec {
cat <<EOF
[Interface]
Address = ${concatStringsSep ", " snCfg.server.externalPeers.${extPeer}}
Address = ${concatStringsSep ", " addresses}
PrivateKey = $privKey
[Peer]

View file

@ -0,0 +1,9 @@
age-encryption.org/v1
-> X25519 whbY47wmwEeXqdKJ7MwjiyAIDpj+fruueMmPTEgnJgY
Z3QdAcWt5mkB3eWZeNkq0eq+UJ5DjL98uciSXb91pVg
-> piv-p256 xqSe8Q AxUBFcdy+TQ/aXS8/1dZWQxbHUbPdjdm6RcM3vyj1qxB
huH3sE7CutLMnL2AA7riZLG2q7vfKHq1yw1cCWIUcGo
-> _-~<XC-grease 6~]V 3\ 3l4
VdckN3SwUIKhL7vUKNG8p3cfd64n+Ac
--- qCoQNOQPvZlq2nU8hwmpk9zGRJe5VLK5/t6DHyQ3omw
SÎÁÁÁăç×<,Ě�ĐË?în•S@s¦†9_ëż2@ŐĆq=‡Q !JÚĘĘLfťśGlÔa?śŁ-ćj+š†3xf€~PxŕOÄ\É:Ć

View file

@ -0,0 +1 @@
ledSlUTiISwDaIZpR1tiWpK0ZQsPOYExCZCAGY5rAC8=

View file

@ -0,0 +1,9 @@
age-encryption.org/v1
-> X25519 Zfuwveyf86nRchq2VM9pUX2GpEJ7fOCD8S/ZpgnohBo
cTqLLXd0WDjeUw8v3Zi7tEu8AuHqGMouNNVMBvDSz/s
-> piv-p256 xqSe8Q AhkBYH9xbuiZzDEEPZKdI+b8cRBwpFynKCcG27IRcxeP
d7JvnhavlhklbmkUna76PL6E+oVVNl8AQs+Y2XgWOLM
-> #+BWuCW-grease -y#r [YV T?;fL)t^ lrGksIs
PWmuTyDWS1KmdmgKW3B7ITyE6Yl/Vb2cTggzNr2rDQ
--- xHLh9TYKLUEcU+rYSNyUomo0H9bNx92gC1To/qTAav8
iEN¸ÖdÝcˆ ç§úMf›×' ª>Æ:û‚;Ý—Ûm¤x0•v'YY(d¹ñV·¦]…¾r”PVÏŠdÉj,N¦r&¬€”C·

View file

@ -0,0 +1 @@
E5VkPLuSW3IJ1fK3FerHCfPc6xyTzD7q8D3AATmWME0=

View file

@ -0,0 +1,10 @@
age-encryption.org/v1
-> X25519 W2AeVTtVkO93zSxX59GwhBy5NwRacz6w0dEk5JptS0Q
QZfGRkvYZjvWoK64RwH/D1pSm+Q5Z/bWa+wCiStim80
-> piv-p256 xqSe8Q A5ODsP5r/eJxRYohpCeC/os0qx+HITx9coafiXkO5aCY
Lfzy5uPK315poUK59pDa9UsyjzY0bf94BvpJQC4qAEQ
-> mF-grease d w /I6vG!UZ 1fNC
QAvTqQEf64QZ9WPtav9CjSYIx8UjIHOMdaPyzKG8OaYa6d8QsrTog1OP7sqJemmE
+1gSHFORe7ofpxrzCFE
--- vbL5PR2z5571aWqnQ6+6Vk8Ni11SDvWtH8dl8i3Z44k
Hl-bS_ Ş/­¶ŔŢ*B+f_şÁíĚąß娛Yř/_„ߡϾDC\I!{ć˛Ď[ŠY±ÎpęÇä¬^Z©ÂGÁ@ZE)¦Nr·ŃÇ