feat: refactor and integrate wireguard module into microvm module

This commit is contained in:
oddlama 2023-05-19 21:10:16 +02:00
parent e5f3ffd288
commit 78cdcd3c69
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
10 changed files with 385 additions and 256 deletions

57
flake.lock generated
View file

@ -3,7 +3,9 @@
"agenix": { "agenix": {
"inputs": { "inputs": {
"darwin": "darwin", "darwin": "darwin",
"home-manager": "home-manager", "home-manager": [
"home-manager"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
@ -54,15 +56,15 @@
"stable": "stable" "stable": "stable"
}, },
"locked": { "locked": {
"lastModified": 1684127527, "lastModified": 1684497694,
"narHash": "sha256-tAzgb2jgmRaX9HETry38h2OvBf9YkHEH1fFvIJQV9A0=", "narHash": "sha256-vFIB57ZqUftCfJcjkzEkvNVAdCbn80A2HXZ2OXl6wtA=",
"owner": "zhaofengli", "owner": "oddlama",
"repo": "colmena", "repo": "colmena",
"rev": "caf33af7d854c8d9b88a8f3dae7adb1c24c1407b", "rev": "888e238953cf4ceb2577b668ea78318849a07529",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "zhaofengli", "owner": "oddlama",
"repo": "colmena", "repo": "colmena",
"type": "github" "type": "github"
} }
@ -117,11 +119,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1684170997, "lastModified": 1684472660,
"narHash": "sha256-WgwqHeYv2sDA0eWghnYCUNx7dm5S8lqDVZjp7ufzm30=", "narHash": "sha256-P4sR6f27FKoQuGnThELALUuJeu9mZ9Zh7/dYdaAd2ek=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "10402e31443941b50bf62e67900743dcb26b3b27", "rev": "efb2016c8e6a91ea64e0604d69e332d8aceabb95",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -204,36 +206,15 @@
"home-manager": { "home-manager": {
"inputs": { "inputs": {
"nixpkgs": [ "nixpkgs": [
"agenix",
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1682203081, "lastModified": 1684484967,
"narHash": "sha256-kRL4ejWDhi0zph/FpebFYhzqlOBrk0Pl3dzGEKSAlEw=", "narHash": "sha256-P3ftCqeJmDYS9LSr2gGC4XGGcp5vv8TOasJX6fVHWsw=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "32d3e39c491e2f91152c84f8ad8b003420eab0a1", "rev": "b9a52ad20e58ebd003444915e35e3dd2c18fc715",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1684157850,
"narHash": "sha256-xGHTCgvAxO5CgAL6IAgE/VGRX2wob2Y+DPyqpXJ32oQ=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "c0deab0effd576e70343cb5df0c64428e0e0d010",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -244,11 +225,11 @@
}, },
"impermanence": { "impermanence": {
"locked": { "locked": {
"lastModified": 1684144492, "lastModified": 1684264534,
"narHash": "sha256-5TBG9kZGdKrZGHdyjLA04ODSzhx1Bx/vwMxfRgWF+JU=", "narHash": "sha256-K0zr+ry3FwIo3rN2U/VWAkCJSgBslBisvfRIPwMbuCQ=",
"owner": "nix-community", "owner": "nix-community",
"repo": "impermanence", "repo": "impermanence",
"rev": "ec1a8e70d61261f9ada30f4e450ea7230d9efb62", "rev": "89253fb1518063556edd5e54509c30ac3089d5e6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -368,7 +349,7 @@
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1684049129, "lastModified": 1684049129,
"narHash": "sha256-dyq0Cc+C/WaVHWSIICqIlteLzzQyRAfw3rQQGrBAzWM=", "narHash": "sha256-FfWznSgzYGFYpbcVcI6QHHiBc8x4EOxaB6U8RtOtFOU=",
"type": "git", "type": "git",
"url": "file:///root/projects/nixpkgs-test" "url": "file:///root/projects/nixpkgs-test"
}, },
@ -428,7 +409,7 @@
"colmena": "colmena", "colmena": "colmena",
"disko": "disko", "disko": "disko",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"home-manager": "home-manager_2", "home-manager": "home-manager",
"impermanence": "impermanence", "impermanence": "impermanence",
"lib-net": "lib-net", "lib-net": "lib-net",
"microvm": "microvm", "microvm": "microvm",

View file

@ -3,7 +3,7 @@
inputs = { inputs = {
colmena = { colmena = {
url = "github:zhaofengli/colmena"; url = "github:oddlama/colmena";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils"; inputs.flake-utils.follows = "flake-utils";
}; };
@ -53,6 +53,7 @@
agenix = { agenix = {
url = "github:ryantm/agenix"; url = "github:ryantm/agenix";
inputs.home-manager.follows = "home-manager";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
agenix-rekey = { agenix-rekey = {

View file

@ -14,7 +14,12 @@
lib.recursiveUpdate libWithNet { lib.recursiveUpdate libWithNet {
net = { net = {
cidr = rec { cidr = rec {
hostCidr = n: x: "${libWithNet.net.cidr.host n x}/${libWithNet.net.cidr.length x}"; 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 = n: x: "${libWithNet.net.cidr.host n x}/${toString (libWithNet.net.cidr.length x)}";
ip = x: lib.head (lib.splitString "/" x); ip = x: lib.head (lib.splitString "/" x);
canonicalize = x: libWithNet.net.cidr.make (libWithNet.net.cidr.length x) (ip x); canonicalize = x: libWithNet.net.cidr.make (libWithNet.net.cidr.length x) (ip x);
}; };
@ -50,6 +55,7 @@
boot = { boot = {
initrd.systemd.enable = true; initrd.systemd.enable = true;
# Add "rd.systemd.unit=rescue.target" to debug initrd
kernelParams = ["log_buf_len=10M"]; kernelParams = ["log_buf_len=10M"];
tmp.useTmpfs = true; tmp.useTmpfs = true;
}; };

View file

@ -3,7 +3,6 @@
inputs, inputs,
lib, lib,
nixos-hardware, nixos-hardware,
nodeSecrets,
pkgs, pkgs,
... ...
}: { }: {
@ -26,20 +25,18 @@
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.microvms = let extra.microvms = {
macOffset = config.lib.net.mac.addPrivate nodeSecrets.networking.interfaces.lan.mac; vms.test = {
in { id = 11;
test = { host = "test.local";
system = "x86_64-linux";
autostart = true;
zfs = { zfs = {
enable = true; enable = true;
pool = "rpool"; pool = "rpool";
dataset = "safe/vms/test"; dataset = "safe/vms/test";
mountpoint = "/persist/vms/test"; mountpoint = "/persist/vms/test";
}; };
autostart = true;
mac = macOffset "00:00:00:00:00:11";
macvtap = "lan";
system = "x86_64-linux";
}; };
}; };
@ -99,10 +96,4 @@
# }; # };
# }; # };
#}; #};
#microvm.vms.agag = {
# flake = self;
# updateFlake = microvm;
#};
#microvm.autostart = ["guest"];
} }

View file

@ -23,7 +23,7 @@
}; };
zpool = with extraLib.disko.zfs; { zpool = with extraLib.disko.zfs; {
rpool = rpool =
encryptedZpool defaultZpoolOptions
// { // {
datasets = { datasets = {
"local" = unmountable; "local" = unmountable;

View file

@ -4,7 +4,7 @@
nodeSecrets, nodeSecrets,
... ...
}: let }: let
inherit (config.lib.net) cidr; inherit (config.lib.net) ip cidr;
net.lan.ipv4cidr = "192.168.100.1/24"; net.lan.ipv4cidr = "192.168.100.1/24";
net.lan.ipv6cidr = "fd00::1/64"; net.lan.ipv6cidr = "fd00::1/64";
@ -94,6 +94,7 @@ in {
zones = lib.mkForce { zones = lib.mkForce {
lan.interfaces = ["lan-self"]; lan.interfaces = ["lan-self"];
wan.interfaces = ["wan"]; wan.interfaces = ["wan"];
"local-vms".interfaces = ["wg-local-vms"];
}; };
rules = lib.mkForce { rules = lib.mkForce {
@ -133,7 +134,6 @@ in {
}; };
}; };
# TODO to microvm!
services.kea = { services.kea = {
dhcp4 = { dhcp4 = {
enable = true; enable = true;
@ -153,7 +153,7 @@ in {
option-data = [ option-data = [
{ {
name = "domain-name-servers"; name = "domain-name-servers";
# TODO pihole self # TODO pihole via self
data = "1.1.1.1, 8.8.8.8"; data = "1.1.1.1, 8.8.8.8";
} }
]; ];
@ -161,10 +161,8 @@ in {
{ {
interface = "lan-self"; interface = "lan-self";
subnet = cidr.canonicalize net.lan.ipv4cidr; subnet = cidr.canonicalize net.lan.ipv4cidr;
# TODO calculate this automatically, start at 40 or so
# to have enough for reservations
pools = [ pools = [
{pool = "192.168.100.20 - 192.168.100.250";} {pool = "${cidr.host 20 net.lan.ipv4cidr} - ${cidr.host (-6) net.lan.ipv4cidr}";}
]; ];
option-data = [ option-data = [
{ {
@ -172,13 +170,6 @@ in {
data = cidr.ip net.lan.ipv4cidr; data = cidr.ip net.lan.ipv4cidr;
} }
]; ];
# TODO reserve addresses for each VM
#reservations = [
# {
# duid = "aa:bb:cc:dd:ee:ff";
# ip-address = cidr.ip net.lan.ipv4cidr;
# }
#];
} }
]; ];
}; };
@ -187,13 +178,9 @@ in {
systemd.services.kea-dhcp4-server.after = ["sys-subsystem-net-devices-lan.device"]; systemd.services.kea-dhcp4-server.after = ["sys-subsystem-net-devices-lan.device"];
#extra.wireguard.vms = { extra.microvms.networking = {
# server = { baseMac = nodeSecrets.networking.interfaces.lan.mac;
# enable = true; host = cidr.ip net.lan.ipv4cidr;
# host = "192.168.1.231"; macvtapInterface = "lan";
# port = 51822; };
# openFirewall = true;
# };
# addresses = ["10.0.0.1/24"];
#};
} }

View file

@ -25,11 +25,13 @@
mkMerge mkMerge
mkOption mkOption
optional optional
optionalAttrs
recursiveUpdate recursiveUpdate
types types
; ;
cfg = config.extra.microvms; cfg = config.extra.microvms;
inherit (config.extra.microvms) vms;
# Configuration for each microvm # Configuration for each microvm
microvmConfig = vmName: vmCfg: { microvmConfig = vmName: vmCfg: {
@ -61,8 +63,16 @@
inherit (vmCfg) system; inherit (vmCfg) system;
config = nodePath + "/microvms/${vmName}"; config = nodePath + "/microvms/${vmName}";
}; };
mac = config.lib.net.mac.addPrivate vmCfg.id cfg.networking.baseMac;
in { in {
inherit (node) pkgs specialArgs; # Allow children microvms to know which node is their parent
specialArgs =
{
parentNode = config;
parentNodeName = nodeName;
}
// node.specialArgs;
inherit (node) pkgs;
inherit (vmCfg) autostart; inherit (vmCfg) autostart;
config = { config = {
imports = [microvm.microvm] ++ node.imports; imports = [microvm.microvm] ++ node.imports;
@ -75,11 +85,11 @@
{ {
type = "macvtap"; type = "macvtap";
id = "vm-${vmName}"; id = "vm-${vmName}";
inherit mac;
macvtap = { macvtap = {
link = vmCfg.macvtap; link = cfg.macvtapInterface;
mode = "bridge"; mode = "bridge";
}; };
inherit (vmCfg) mac;
} }
]; ];
@ -114,7 +124,7 @@
gc.automatic = mkForce false; gc.automatic = mkForce false;
}; };
extra.networking.renameInterfacesByMac.${vmCfg.linkName} = vmCfg.mac; extra.networking.renameInterfacesByMac.${vmCfg.linkName} = mac;
systemd.network.networks = { systemd.network.networks = {
"10-${vmCfg.linkName}" = { "10-${vmCfg.linkName}" = {
@ -130,6 +140,46 @@
# TODO change once microvms are compatible with stage-1 systemd # TODO change once microvms are compatible with stage-1 systemd
boot.initrd.systemd.enable = mkForce false; boot.initrd.systemd.enable = mkForce false;
# Create a firewall zone for the bridged traffic and secure vm traffic
networking.nftables.firewall = {
zones = lib.mkForce {
"${vmCfg.linkName}".interfaces = [vmCfg.linkName];
"local-vms".interfaces = ["wg-local-vms"];
};
rules = lib.mkForce {
"${vmCfg.linkName}-to-local" = {
from = [vmCfg.linkName];
to = ["local"];
};
local-vms-to-local = {
from = ["wg-local-vms"];
to = ["local"];
};
};
};
extra.wireguard."local-vms" = {
# We have a resolvable hostname / static ip, so all peers can directly communicate with us
server = optionalAttrs (cfg.networking.host != null) {
inherit (vmCfg) host;
port = 51829;
openFirewallInRules = ["${vmCfg.linkName}-to-local"];
};
# We have no static hostname, so we must use a client-server architecture.
client = optionalAttrs (cfg.networking.host == null) {
via = nodeName;
keepalive = false;
};
# TODO check error: addresses = ["10.22.22.2/30"];
# TODO switch wg module to explicit v4 and v6
addresses = [
"${config.lib.net.cidr.host vmCfg.id cfg.networking.wireguard.netv4}/32"
"${config.lib.net.cidr.host vmCfg.id cfg.networking.wireguard.netv6}/128"
];
};
}; };
}; };
}; };
@ -138,14 +188,72 @@ in {
# Add the host module, but only enable if it necessary # Add the host module, but only enable if it necessary
microvm.host microvm.host
# This is opt-out, so we can't put this into the mkIf below # This is opt-out, so we can't put this into the mkIf below
{microvm.host.enable = cfg != {};} {microvm.host.enable = vms != {};}
]; ];
options.extra.microvms = mkOption { options.extra.microvms = {
networking = {
baseMac = mkOption {
type = config.lib.net.types.mac;
description = mdDoc ''
This MAC address will be used as a base address to derive all MicroVM MAC addresses from.
A good practise is to use the physical address of the macvtap interface.
'';
};
host = mkOption {
type = types.str;
description = mdDoc ''
The host as which this machine can be reached from other participants of the bridged macvtap network.
This can either be a resolvable hostname or an IP address.
'';
};
macvtapInterface = mkOption {
type = types.str;
description = mdDoc "The macvtap interface to which MicroVMs should be attached";
};
wireguard = {
netv4 = mkOption {
type = config.lib.net.types.cidrv4;
description = mdDoc "The ipv4 network address range to use for internal vm traffic.";
default = "172.31.0.0/24";
};
netv6 = mkOption {
type = config.lib.net.types.cidrv6;
description = mdDoc "The ipv6 network address range to use for internal vm traffic.";
default = "fddd::/64";
};
};
# TODO check plus no overflow
};
vms = mkOption {
default = {}; default = {};
description = "Handles the necessary base setup for MicroVMs."; description = "Defines the actual vms and handles the necessary base setup for them.";
type = types.attrsOf (types.submodule { type = types.attrsOf (types.submodule {
options = { options = {
id = mkOption {
type =
types.addCheck types.int (x: x > 1)
// {
name = "positiveInt1";
description = "positive integer greater than 1";
};
description = mdDoc ''
A unique id for this VM. It will be used to derive a MAC address from the host's
base MAC, and may be used as a stable id by your MicroVM config if necessary.
Ids don't need to be contiguous. It is recommended to use small numbers here to not
overflow any offset calculations. Consider that this is used for example to determine a
static ip-address by means of (baseIp + vm.id) for a wireguard network. That's also
why id 1 is reserved for the host. While this is usually checked to be in-range,
it might still be a good idea to assign greater ids with care.
'';
};
zfs = { zfs = {
enable = mkEnableOption (mdDoc "Enable persistent data on separate zfs dataset"); enable = mkEnableOption (mdDoc "Enable persistent data on separate zfs dataset");
@ -171,20 +279,25 @@ in {
description = mdDoc "Whether this VM should be started automatically with the host"; description = mdDoc "Whether this VM should be started automatically with the host";
}; };
# TODO allow configuring static ipv4 and ipv6 instead of dhcp?
# maybe create networking. namespace and have options = dhcpwithRA and static.
linkName = mkOption { linkName = mkOption {
type = types.str; type = types.str;
default = "wan"; default = "wan";
description = mdDoc "The main ethernet link name inside of the VM"; description = mdDoc "The main ethernet link name inside of the VM";
}; };
mac = mkOption { host = mkOption {
type = config.lib.net.types.mac; type = types.nullOr types.str;
description = mdDoc "The MAC address to assign to this VM"; default = null;
}; description = mdDoc ''
The host as which this VM can be reached from other participants of the bridged macvtap network.
If this is unset, the wireguard connection will use a client-server architecture with the host as the server.
Otherwise, all clients will communicate directly, meaning the host cannot listen to traffic.
macvtap = mkOption { This can either be a resolvable hostname or an IP address.
type = types.str; '';
description = mdDoc "The macvtap interface to attach to";
}; };
system = mkOption { system = mkOption {
@ -194,22 +307,32 @@ in {
}; };
}); });
}; };
};
config = mkIf (cfg != {}) ( config = mkIf (vms != {}) (
{ {
assertions = let assertions = let
duplicateMacs = extraLib.duplicates (mapAttrsToList (_: vmCfg: vmCfg.mac) cfg); duplicateIds = extraLib.duplicates (mapAttrsToList (_: vmCfg: toString vmCfg.id) vms);
in [ in [
{ {
assertion = duplicateMacs == []; assertion = duplicateIds == [];
message = "Duplicate MicroVM MAC addresses: ${concatStringsSep ", " duplicateMacs}"; message = "Duplicate MicroVM ids: ${concatStringsSep ", " duplicateIds}";
} }
]; ];
# Define a local wireguard server to communicate with vms securely
extra.wireguard."local-vms" = {
server = {
inherit (cfg.networking) host;
port = 51829;
openFirewallInRules = ["lan-to-local"];
};
addresses = [
(config.lib.net.cidr.hostCidr 1 cfg.networking.wireguard.netv4)
(config.lib.net.cidr.hostCidr 1 cfg.networking.wireguard.netv6)
];
};
} }
// lib.genAttrs ["disko" "microvm" "systemd"] // extraLib.mergeToplevelConfigs ["disko" "microvm" "systemd"] (mapAttrsToList microvmConfig vms)
(attr:
mkMerge (map
(c: c.${attr})
(mapAttrsToList microvmConfig cfg)))
); );
} }

View file

@ -17,14 +17,13 @@
concatStringsSep concatStringsSep
filter filter
filterAttrs filterAttrs
genAttrs
head head
mapAttrsToList mapAttrsToList
mdDoc mdDoc
mergeAttrs mergeAttrs
mkIf mkIf
mkOption mkOption
mkEnableOption
net
optionalAttrs optionalAttrs
optionals optionals
splitString splitString
@ -35,57 +34,102 @@
(extraLib) (extraLib)
concatAttrs concatAttrs
duplicates duplicates
mergeToplevelConfigs
; ;
inherit (config.lib) net;
cfg = config.extra.wireguard; cfg = config.extra.wireguard;
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
# TODO use netlib types!!!!!!
configForNetwork = wgName: wgCfg: let configForNetwork = wgName: wgCfg: let
inherit inherit
(extraLib.wireguard wgName) (extraLib.wireguard wgName)
associatedNodes
associatedServerNodes associatedServerNodes
associatedClientNodes associatedClientNodes
externalPeerName externalPeerName
externalPeerNamesRaw
peerPresharedKeyPath peerPresharedKeyPath
peerPresharedKeySecret peerPresharedKeySecret
peerPrivateKeyPath peerPrivateKeyPath
peerPrivateKeySecret peerPrivateKeySecret
peerPublicKeyPath peerPublicKeyPath
usedAddresses
; ;
isServer = wgCfg.server.host != null;
isClient = wgCfg.client.via != null;
filterSelf = filter (x: x != nodeName); filterSelf = filter (x: x != nodeName);
wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName}; wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName};
# All nodes that use our node as the via into the wireguard network
ourClientNodes = ourClientNodes =
optionals wgCfg.server.enable optionals isServer
(filter (n: (wgCfgOf n).via == nodeName) associatedClientNodes); (filter (n: (wgCfgOf n).client.via == nodeName) associatedClientNodes);
# The list of peers that we have to know the psk to. # The list of peers for which we have to know the psk.
neededPeers = neededPeers =
if wgCfg.server.enable if isServer
then then
# Other servers in the same network
filterSelf associatedServerNodes filterSelf associatedServerNodes
# Our external peers
++ map externalPeerName (attrNames wgCfg.server.externalPeers) ++ map externalPeerName (attrNames wgCfg.server.externalPeers)
# Our clients
++ ourClientNodes ++ ourClientNodes
else [wgCfg.via]; else [wgCfg.client.via];
in {
secrets =
concatAttrs (map (other: {
${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other;
})
neededPeers)
// {
${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName;
};
netdevs."${wgCfg.priority}-${wgName}" = { # 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);
# Adds context information to the assertions for this network
assertionPrefix = "Wireguard network '${wgName}' on '${nodeName}'";
in {
assertions = [
{
assertion = any (n: (wgCfgOf n).server.host != null) associatedNodes;
message = "${assertionPrefix}: At least one node in a network must be a server.";
}
{
assertion = duplicatePeers == [];
message = "${assertionPrefix}: Multiple definitions for external peer(s):${concatMapStrings (x: " '${x}'") duplicatePeers}";
}
{
assertion = duplicateAddrs == [];
message = "${assertionPrefix}: Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}";
}
{
assertion = isServer != isClient;
message = "${assertionPrefix}: A node must either be a server (define server.host) or a client (define client.via).";
}
{
assertion = isClient -> ((wgCfgOf wgCfg.client.via).server.host != null);
message = "${assertionPrefix}: The specified via node '${wgCfg.client.via}' must be a wireguard server.";
}
# TODO externalPeers != {} -> ip forwarding
# TODO no overlapping cidrs in (external peers + peers using via = this).
# TODO no overlapping cidrs between server nodes
];
networking.firewall.allowedUDPPorts =
mkIf
(isServer && wgCfg.server.openFirewall)
[wgCfg.server.port];
networking.nftables.firewall.rules =
mkIf
(isServer && wgCfg.server.openFirewallInRules != [])
(genAttrs wgCfg.server.openFirewallInRules (_: {allowedUDPPorts = [wgCfg.server.port];}));
rekey.secrets =
concatAttrs (map
(other: {${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other;})
neededPeers)
// {${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName;};
systemd.network.netdevs."${toString wgCfg.priority}-${wgName}" = {
netdevConfig = { netdevConfig = {
Kind = "wireguard"; Kind = "wireguard";
Name = "${wgName}"; Name = "${wgName}";
@ -95,17 +139,17 @@
{ {
PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path; PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path;
} }
// optionalAttrs wgCfg.server.enable { // optionalAttrs isServer {
ListenPort = wgCfg.server.port; ListenPort = wgCfg.server.port;
}; };
wireguardPeers = wireguardPeers =
if wgCfg.server.enable if isServer
then then
# Always include all other server nodes. # Always include all other server nodes.
map (serverNode: let map (serverNode: {
wireguardPeerConfig = let
snCfg = wgCfgOf serverNode; snCfg = wgCfgOf serverNode;
in { in {
wireguardPeerConfig = {
PublicKey = builtins.readFile (peerPublicKeyPath serverNode); PublicKey = builtins.readFile (peerPublicKeyPath serverNode);
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName serverNode}.path; PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName serverNode}.path;
# The allowed ips of a server node are it's own addreses, # The allowed ips of a server node are it's own addreses,
@ -117,7 +161,8 @@
# ++ map (n: (wgCfgOf n).addresses) snCfg.ourClientNodes; # ++ map (n: (wgCfgOf n).addresses) snCfg.ourClientNodes;
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}"; Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
}; };
}) (filterSelf associatedServerNodes) })
(filterSelf associatedServerNodes)
# All our external peers # All our external peers
++ mapAttrsToList (extPeer: allowedIPs: let ++ mapAttrsToList (extPeer: allowedIPs: let
peerName = externalPeerName extPeer; peerName = externalPeerName extPeer;
@ -126,16 +171,22 @@
PublicKey = builtins.readFile (peerPublicKeyPath peerName); PublicKey = builtins.readFile (peerPublicKeyPath peerName);
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path; PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path;
AllowedIPs = allowedIPs; AllowedIPs = allowedIPs;
# Connections to external peers should always be kept alive
PersistentKeepalive = 25; PersistentKeepalive = 25;
}; };
}) })
wgCfg.server.externalPeers wgCfg.server.externalPeers
# All client nodes that have their via set to us. # All client nodes that have their via set to us.
++ mapAttrsToList (clientNode: { ++ mapAttrsToList (clientNode: let
wireguardPeerConfig = { clientCfg = wgCfgOf clientNode;
in {
wireguardPeerConfig =
{
PublicKey = builtins.readFile (peerPublicKeyPath clientNode); PublicKey = builtins.readFile (peerPublicKeyPath clientNode);
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName clientNode}.path; PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName clientNode}.path;
AllowedIPs = (wgCfgOf clientNode).addresses; AllowedIPs = clientCfg.addresses;
}
// optionalAttrs clientCfg.keepalive {
PersistentKeepalive = 25; PersistentKeepalive = 25;
}; };
}) })
@ -145,15 +196,16 @@
[ [
{ {
wireguardPeerConfig = { wireguardPeerConfig = {
PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.via); PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.client.via);
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName wgCfg.via}.path; PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName wgCfg.client.via}.path;
AllowedIPs = (wgCfgOf wgCfg.via).addresses; # TODO this should be 0.0.0.0 if the client wants to route all traffic
AllowedIPs = (wgCfgOf wgCfg.client.via).addresses;
}; };
} }
]; ];
}; };
networks."${wgCfg.priority}-${wgName}" = { systemd.network.networks."${toString wgCfg.priority}-${wgName}" = {
matchConfig.Name = wgName; matchConfig.Name = wgName;
networkConfig.Address = wgCfg.addresses; networkConfig.Address = wgCfg.addresses;
}; };
@ -162,14 +214,17 @@ in {
options.extra.wireguard = mkOption { options.extra.wireguard = mkOption {
default = {}; default = {};
description = "Configures wireguard networks via systemd-networkd."; description = "Configures wireguard networks via systemd-networkd.";
type = types.attrsOf (types.submodule { type = types.lazyAttrsOf (types.submodule ({
config,
name,
...
}: {
options = { options = {
server = { server = {
enable = mkEnableOption (mdDoc "wireguard server");
host = mkOption { host = mkOption {
type = types.str; default = null;
description = mdDoc "The hostname or ip address which other peers can use to reach this host."; type = types.nullOr types.str;
description = mdDoc "The hostname or ip address which other peers can use to reach this host. No server funnctionality will be activated if set to null.";
}; };
port = mkOption { port = mkOption {
@ -181,11 +236,17 @@ in {
openFirewall = mkOption { openFirewall = mkOption {
default = false; default = false;
type = types.bool; type = types.bool;
description = mdDoc "Whether to open the firewall for the specified `listenPort`, if {option}`listen` is `true`."; description = mdDoc "Whether to open the firewall for the specified {option}`port`.";
};
openFirewallInRules = 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 { externalPeers = mkOption {
type = types.attrsOf (types.listOf types.str); type = types.attrsOf (types.listOf (net.types.cidr-in config.addresses));
default = {}; default = {};
example = {my-android-phone = ["10.0.0.97/32"];}; example = {my-android-phone = ["10.0.0.97/32"];};
description = mdDoc '' description = mdDoc ''
@ -199,82 +260,55 @@ in {
}; };
}; };
client = {
via = mkOption {
default = null;
type = types.nullOr types.str;
description = mdDoc ''
The server node via which to connect to the network.
No client functionality will be activated if set to null.
'';
};
keepalive = mkOption {
default = true;
type = types.bool;
description = mdDoc "Whether to keep this connection alive using PersistentKeepalive. Set to false only for networks where client and server IPs are stable.";
};
# TODO one option for allowing it, but also one to allow defining two
# profiles / interfaces that can be activated manually.
#routeAllTraffic = mkOption {
# default = false;
# type = types.bool;
# description = mdDoc ''
# Whether to allow routing all traffic through the via server.
# '';
#};
};
priority = mkOption { priority = mkOption {
default = "20"; default = 40;
type = types.str; type = types.int;
description = mdDoc "The order priority used when creating systemd netdev and network files."; description = mdDoc "The order priority used when creating systemd netdev and network files.";
}; };
via = mkOption {
default = null;
type = types.uniq (types.nullOr types.str);
description = mdDoc ''
The server node via which to connect to the network.
This must defined if and only if this node is not a server.
'';
};
addresses = mkOption { addresses = mkOption {
type = types.listOf types.str; type = types.listOf (
if config.client.via != null
then net.types.cidr-in nodes.${config.client.via}.config.extra.wireguard.${name}.addresses
else net.types.cidr
);
description = mdDoc '' description = mdDoc ''
The addresses to configure for this interface. Will automatically be added The addresses to configure for this interface. Will automatically be added
as this peer's allowed addresses to all other peers. as this peer's allowed addresses on all other peers.
''; '';
}; };
}; };
}); }));
}; };
config = mkIf (cfg != {}) (let config = mkIf (cfg != {}) (mergeToplevelConfigs
networkCfgs = mapAttrsToList configForNetwork cfg; ["assertions" "rekey" "networking" "systemd"]
collectAllNetworkAttrs = x: concatAttrs (map (y: y.${x}) networkCfgs); (mapAttrsToList configForNetwork cfg));
in {
assertions = concatMap (wgName: let
inherit
(extraLib.wireguard wgName)
externalPeerNamesRaw
usedAddresses
associatedNodes
;
wgCfg = cfg.${wgName};
wgCfgOf = node: nodes.${node}.config.extra.wireguard.${wgName};
duplicatePeers = duplicates externalPeerNamesRaw;
duplicateAddrs = duplicates (map (x: head (splitString "/" x)) usedAddresses);
in [
{
assertion = any (n: nodes.${n}.config.extra.wireguard.${wgName}.server.enable) associatedNodes;
message = "Wireguard network '${wgName}': At least one node must be a server.";
}
{
assertion = duplicatePeers == [];
message = "Wireguard network '${wgName}': Multiple definitions for external peer(s):${concatMapStrings (x: " '${x}'") duplicatePeers}";
}
{
assertion = duplicateAddrs == [];
message = "Wireguard network '${wgName}': Addresses used multiple times: ${concatStringsSep ", " duplicateAddrs}";
}
{
assertion = wgCfg.server.externalPeers != {} -> wgCfg.server.enable;
message = "Wireguard network '${wgName}': Defining external peers requires server.enable = true.";
}
{
assertion = wgCfg.server.enable == (wgCfg.via == null);
message = "Wireguard network '${wgName}': A via server must be defined exactly iff this isn't a server node.";
}
{
assertion = wgCfg.via != null -> (wgCfgOf wgCfg.via).server.enable;
message = "Wireguard network '${wgName}': The specified via node '${wgCfg.via}' must be a wireguard server.";
}
# TODO externalPeers != {} -> ip forwarding
# TODO no overlapping allowed ip range? 0.0.0.0 would be ok to overlap though
]) (attrNames cfg);
networking.firewall.allowedUDPPorts = mkIf (cfg.server.enable && cfg.server.openFirewall) [cfg.server.port];
rekey.secrets = collectAllNetworkAttrs "secrets";
systemd.network = {
netdevs = collectAllNetworkAttrs "netdevs";
networks = collectAllNetworkAttrs "networks";
};
});
} }

View file

@ -10,7 +10,7 @@
# plugin-files = ${pkgs.nix-plugins}/lib/nix/plugins # plugin-files = ${pkgs.nix-plugins}/lib/nix/plugins
# # Please adjust path accordingly, or leave this out and alternativaly # # Please adjust path accordingly, or leave this out and alternativaly
# # pass `--option extra-builtins-file ./extra-builtins.nix` to each invocation # # pass `--option extra-builtins-file ./extra-builtins.nix` to each invocation
# extra-builtins-file = ./extra-builtins.nix # extra-builtins-file = ${./extra-builtins.nix}
# ''; # '';
# } # }
# ``` # ```

View file

@ -19,6 +19,7 @@
head head
mapAttrs' mapAttrs'
mergeAttrs mergeAttrs
mkMerge
nameValuePair nameValuePair
optionalAttrs optionalAttrs
partition partition
@ -50,6 +51,11 @@ in rec {
# True if the path or string starts with / # True if the path or string starts with /
isAbsolutePath = x: substring 0 1 x == "/"; isAbsolutePath = x: substring 0 1 x == "/";
# Merges all given attributes from the given attrsets using mkMerge.
# Useful to merge several top-level configs in a module.
mergeToplevelConfigs = keys: attrs:
genAttrs keys (attr: mkMerge (map (x: x.${attr} or {}) attrs));
disko = { disko = {
gpt = { gpt = {
partEfi = name: start: end: { partEfi = name: start: end: {
@ -85,7 +91,7 @@ in rec {
}; };
}; };
zfs = { zfs = {
encryptedZpool = { defaultZpoolOptions = {
type = "zpool"; type = "zpool";
mountRoot = "/mnt"; mountRoot = "/mnt";
rootFsOptions = { rootFsOptions = {
@ -164,7 +170,7 @@ in rec {
# Partition nodes by whether they are servers # Partition nodes by whether they are servers
_associatedNodes_isServerPartition = _associatedNodes_isServerPartition =
partition partition
(n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.enable) (n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.host != null)
associatedNodes; associatedNodes;
associatedServerNodes = _associatedNodes_isServerPartition.right; associatedServerNodes = _associatedNodes_isServerPartition.right;