forked from mirrors_public/oddlama_nix-config
feat: refactor and integrate wireguard module into microvm module
This commit is contained in:
parent
e5f3ffd288
commit
78cdcd3c69
10 changed files with 385 additions and 256 deletions
57
flake.lock
generated
57
flake.lock
generated
|
@ -3,7 +3,9 @@
|
|||
"agenix": {
|
||||
"inputs": {
|
||||
"darwin": "darwin",
|
||||
"home-manager": "home-manager",
|
||||
"home-manager": [
|
||||
"home-manager"
|
||||
],
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
|
@ -54,15 +56,15 @@
|
|||
"stable": "stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1684127527,
|
||||
"narHash": "sha256-tAzgb2jgmRaX9HETry38h2OvBf9YkHEH1fFvIJQV9A0=",
|
||||
"owner": "zhaofengli",
|
||||
"lastModified": 1684497694,
|
||||
"narHash": "sha256-vFIB57ZqUftCfJcjkzEkvNVAdCbn80A2HXZ2OXl6wtA=",
|
||||
"owner": "oddlama",
|
||||
"repo": "colmena",
|
||||
"rev": "caf33af7d854c8d9b88a8f3dae7adb1c24c1407b",
|
||||
"rev": "888e238953cf4ceb2577b668ea78318849a07529",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "zhaofengli",
|
||||
"owner": "oddlama",
|
||||
"repo": "colmena",
|
||||
"type": "github"
|
||||
}
|
||||
|
@ -117,11 +119,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1684170997,
|
||||
"narHash": "sha256-WgwqHeYv2sDA0eWghnYCUNx7dm5S8lqDVZjp7ufzm30=",
|
||||
"lastModified": 1684472660,
|
||||
"narHash": "sha256-P4sR6f27FKoQuGnThELALUuJeu9mZ9Zh7/dYdaAd2ek=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "10402e31443941b50bf62e67900743dcb26b3b27",
|
||||
"rev": "efb2016c8e6a91ea64e0604d69e332d8aceabb95",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -204,36 +206,15 @@
|
|||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"agenix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1682203081,
|
||||
"narHash": "sha256-kRL4ejWDhi0zph/FpebFYhzqlOBrk0Pl3dzGEKSAlEw=",
|
||||
"lastModified": 1684484967,
|
||||
"narHash": "sha256-P3ftCqeJmDYS9LSr2gGC4XGGcp5vv8TOasJX6fVHWsw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "32d3e39c491e2f91152c84f8ad8b003420eab0a1",
|
||||
"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",
|
||||
"rev": "b9a52ad20e58ebd003444915e35e3dd2c18fc715",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -244,11 +225,11 @@
|
|||
},
|
||||
"impermanence": {
|
||||
"locked": {
|
||||
"lastModified": 1684144492,
|
||||
"narHash": "sha256-5TBG9kZGdKrZGHdyjLA04ODSzhx1Bx/vwMxfRgWF+JU=",
|
||||
"lastModified": 1684264534,
|
||||
"narHash": "sha256-K0zr+ry3FwIo3rN2U/VWAkCJSgBslBisvfRIPwMbuCQ=",
|
||||
"owner": "nix-community",
|
||||
"repo": "impermanence",
|
||||
"rev": "ec1a8e70d61261f9ada30f4e450ea7230d9efb62",
|
||||
"rev": "89253fb1518063556edd5e54509c30ac3089d5e6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -368,7 +349,7 @@
|
|||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1684049129,
|
||||
"narHash": "sha256-dyq0Cc+C/WaVHWSIICqIlteLzzQyRAfw3rQQGrBAzWM=",
|
||||
"narHash": "sha256-FfWznSgzYGFYpbcVcI6QHHiBc8x4EOxaB6U8RtOtFOU=",
|
||||
"type": "git",
|
||||
"url": "file:///root/projects/nixpkgs-test"
|
||||
},
|
||||
|
@ -428,7 +409,7 @@
|
|||
"colmena": "colmena",
|
||||
"disko": "disko",
|
||||
"flake-utils": "flake-utils",
|
||||
"home-manager": "home-manager_2",
|
||||
"home-manager": "home-manager",
|
||||
"impermanence": "impermanence",
|
||||
"lib-net": "lib-net",
|
||||
"microvm": "microvm",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
inputs = {
|
||||
colmena = {
|
||||
url = "github:zhaofengli/colmena";
|
||||
url = "github:oddlama/colmena";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.flake-utils.follows = "flake-utils";
|
||||
};
|
||||
|
@ -53,6 +53,7 @@
|
|||
|
||||
agenix = {
|
||||
url = "github:ryantm/agenix";
|
||||
inputs.home-manager.follows = "home-manager";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
agenix-rekey = {
|
||||
|
|
|
@ -14,7 +14,12 @@
|
|||
lib.recursiveUpdate libWithNet {
|
||||
net = {
|
||||
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);
|
||||
canonicalize = x: libWithNet.net.cidr.make (libWithNet.net.cidr.length x) (ip x);
|
||||
};
|
||||
|
@ -50,6 +55,7 @@
|
|||
|
||||
boot = {
|
||||
initrd.systemd.enable = true;
|
||||
# Add "rd.systemd.unit=rescue.target" to debug initrd
|
||||
kernelParams = ["log_buf_len=10M"];
|
||||
tmp.useTmpfs = true;
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
inputs,
|
||||
lib,
|
||||
nixos-hardware,
|
||||
nodeSecrets,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
|
@ -26,20 +25,18 @@
|
|||
|
||||
boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sdhci_pci" "r8169"];
|
||||
|
||||
extra.microvms = let
|
||||
macOffset = config.lib.net.mac.addPrivate nodeSecrets.networking.interfaces.lan.mac;
|
||||
in {
|
||||
test = {
|
||||
extra.microvms = {
|
||||
vms.test = {
|
||||
id = 11;
|
||||
host = "test.local";
|
||||
system = "x86_64-linux";
|
||||
autostart = true;
|
||||
zfs = {
|
||||
enable = true;
|
||||
pool = "rpool";
|
||||
dataset = "safe/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"];
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
};
|
||||
zpool = with extraLib.disko.zfs; {
|
||||
rpool =
|
||||
encryptedZpool
|
||||
defaultZpoolOptions
|
||||
// {
|
||||
datasets = {
|
||||
"local" = unmountable;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
nodeSecrets,
|
||||
...
|
||||
}: let
|
||||
inherit (config.lib.net) cidr;
|
||||
inherit (config.lib.net) ip cidr;
|
||||
|
||||
net.lan.ipv4cidr = "192.168.100.1/24";
|
||||
net.lan.ipv6cidr = "fd00::1/64";
|
||||
|
@ -94,6 +94,7 @@ in {
|
|||
zones = lib.mkForce {
|
||||
lan.interfaces = ["lan-self"];
|
||||
wan.interfaces = ["wan"];
|
||||
"local-vms".interfaces = ["wg-local-vms"];
|
||||
};
|
||||
|
||||
rules = lib.mkForce {
|
||||
|
@ -133,7 +134,6 @@ in {
|
|||
};
|
||||
};
|
||||
|
||||
# TODO to microvm!
|
||||
services.kea = {
|
||||
dhcp4 = {
|
||||
enable = true;
|
||||
|
@ -153,7 +153,7 @@ in {
|
|||
option-data = [
|
||||
{
|
||||
name = "domain-name-servers";
|
||||
# TODO pihole self
|
||||
# TODO pihole via self
|
||||
data = "1.1.1.1, 8.8.8.8";
|
||||
}
|
||||
];
|
||||
|
@ -161,10 +161,8 @@ in {
|
|||
{
|
||||
interface = "lan-self";
|
||||
subnet = cidr.canonicalize net.lan.ipv4cidr;
|
||||
# TODO calculate this automatically, start at 40 or so
|
||||
# to have enough for reservations
|
||||
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 = [
|
||||
{
|
||||
|
@ -172,13 +170,6 @@ in {
|
|||
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"];
|
||||
|
||||
#extra.wireguard.vms = {
|
||||
# server = {
|
||||
# enable = true;
|
||||
# host = "192.168.1.231";
|
||||
# port = 51822;
|
||||
# openFirewall = true;
|
||||
# };
|
||||
# addresses = ["10.0.0.1/24"];
|
||||
#};
|
||||
extra.microvms.networking = {
|
||||
baseMac = nodeSecrets.networking.interfaces.lan.mac;
|
||||
host = cidr.ip net.lan.ipv4cidr;
|
||||
macvtapInterface = "lan";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -25,11 +25,13 @@
|
|||
mkMerge
|
||||
mkOption
|
||||
optional
|
||||
optionalAttrs
|
||||
recursiveUpdate
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.extra.microvms;
|
||||
inherit (config.extra.microvms) vms;
|
||||
|
||||
# Configuration for each microvm
|
||||
microvmConfig = vmName: vmCfg: {
|
||||
|
@ -61,8 +63,16 @@
|
|||
inherit (vmCfg) system;
|
||||
config = nodePath + "/microvms/${vmName}";
|
||||
};
|
||||
mac = config.lib.net.mac.addPrivate vmCfg.id cfg.networking.baseMac;
|
||||
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;
|
||||
config = {
|
||||
imports = [microvm.microvm] ++ node.imports;
|
||||
|
@ -75,11 +85,11 @@
|
|||
{
|
||||
type = "macvtap";
|
||||
id = "vm-${vmName}";
|
||||
inherit mac;
|
||||
macvtap = {
|
||||
link = vmCfg.macvtap;
|
||||
link = cfg.macvtapInterface;
|
||||
mode = "bridge";
|
||||
};
|
||||
inherit (vmCfg) mac;
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -114,7 +124,7 @@
|
|||
gc.automatic = mkForce false;
|
||||
};
|
||||
|
||||
extra.networking.renameInterfacesByMac.${vmCfg.linkName} = vmCfg.mac;
|
||||
extra.networking.renameInterfacesByMac.${vmCfg.linkName} = mac;
|
||||
|
||||
systemd.network.networks = {
|
||||
"10-${vmCfg.linkName}" = {
|
||||
|
@ -130,6 +140,46 @@
|
|||
|
||||
# TODO change once microvms are compatible with stage-1 systemd
|
||||
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
|
||||
microvm.host
|
||||
# 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 = {};
|
||||
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 {
|
||||
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 = {
|
||||
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";
|
||||
};
|
||||
|
||||
# TODO allow configuring static ipv4 and ipv6 instead of dhcp?
|
||||
# maybe create networking. namespace and have options = dhcpwithRA and static.
|
||||
|
||||
linkName = mkOption {
|
||||
type = types.str;
|
||||
default = "wan";
|
||||
description = mdDoc "The main ethernet link name inside of the VM";
|
||||
};
|
||||
|
||||
mac = mkOption {
|
||||
type = config.lib.net.types.mac;
|
||||
description = mdDoc "The MAC address to assign to this VM";
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
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 {
|
||||
type = types.str;
|
||||
description = mdDoc "The macvtap interface to attach to";
|
||||
This can either be a resolvable hostname or an IP address.
|
||||
'';
|
||||
};
|
||||
|
||||
system = mkOption {
|
||||
|
@ -194,22 +307,32 @@ in {
|
|||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf (cfg != {}) (
|
||||
config = mkIf (vms != {}) (
|
||||
{
|
||||
assertions = let
|
||||
duplicateMacs = extraLib.duplicates (mapAttrsToList (_: vmCfg: vmCfg.mac) cfg);
|
||||
duplicateIds = extraLib.duplicates (mapAttrsToList (_: vmCfg: toString vmCfg.id) vms);
|
||||
in [
|
||||
{
|
||||
assertion = duplicateMacs == [];
|
||||
message = "Duplicate MicroVM MAC addresses: ${concatStringsSep ", " duplicateMacs}";
|
||||
assertion = duplicateIds == [];
|
||||
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"]
|
||||
(attr:
|
||||
mkMerge (map
|
||||
(c: c.${attr})
|
||||
(mapAttrsToList microvmConfig cfg)))
|
||||
// extraLib.mergeToplevelConfigs ["disko" "microvm" "systemd"] (mapAttrsToList microvmConfig vms)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,14 +17,13 @@
|
|||
concatStringsSep
|
||||
filter
|
||||
filterAttrs
|
||||
genAttrs
|
||||
head
|
||||
mapAttrsToList
|
||||
mdDoc
|
||||
mergeAttrs
|
||||
mkIf
|
||||
mkOption
|
||||
mkEnableOption
|
||||
net
|
||||
optionalAttrs
|
||||
optionals
|
||||
splitString
|
||||
|
@ -35,57 +34,102 @@
|
|||
(extraLib)
|
||||
concatAttrs
|
||||
duplicates
|
||||
mergeToplevelConfigs
|
||||
;
|
||||
|
||||
inherit (config.lib) net;
|
||||
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
|
||||
inherit
|
||||
(extraLib.wireguard wgName)
|
||||
associatedNodes
|
||||
associatedServerNodes
|
||||
associatedClientNodes
|
||||
externalPeerName
|
||||
externalPeerNamesRaw
|
||||
peerPresharedKeyPath
|
||||
peerPresharedKeySecret
|
||||
peerPrivateKeyPath
|
||||
peerPrivateKeySecret
|
||||
peerPublicKeyPath
|
||||
usedAddresses
|
||||
;
|
||||
|
||||
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 =
|
||||
optionals wgCfg.server.enable
|
||||
(filter (n: (wgCfgOf n).via == nodeName) associatedClientNodes);
|
||||
optionals isServer
|
||||
(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 =
|
||||
if wgCfg.server.enable
|
||||
if isServer
|
||||
then
|
||||
# Other servers in the same network
|
||||
filterSelf associatedServerNodes
|
||||
# Our external peers
|
||||
++ map externalPeerName (attrNames wgCfg.server.externalPeers)
|
||||
# Our clients
|
||||
++ ourClientNodes
|
||||
else [wgCfg.via];
|
||||
in {
|
||||
secrets =
|
||||
concatAttrs (map (other: {
|
||||
${peerPresharedKeySecret nodeName other}.file = peerPresharedKeyPath nodeName other;
|
||||
})
|
||||
neededPeers)
|
||||
// {
|
||||
${peerPrivateKeySecret nodeName}.file = peerPrivateKeyPath nodeName;
|
||||
};
|
||||
else [wgCfg.client.via];
|
||||
|
||||
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 = {
|
||||
Kind = "wireguard";
|
||||
Name = "${wgName}";
|
||||
|
@ -95,17 +139,17 @@
|
|||
{
|
||||
PrivateKeyFile = config.rekey.secrets.${peerPrivateKeySecret nodeName}.path;
|
||||
}
|
||||
// optionalAttrs wgCfg.server.enable {
|
||||
// optionalAttrs isServer {
|
||||
ListenPort = wgCfg.server.port;
|
||||
};
|
||||
wireguardPeers =
|
||||
if wgCfg.server.enable
|
||||
if isServer
|
||||
then
|
||||
# Always include all other server nodes.
|
||||
map (serverNode: let
|
||||
map (serverNode: {
|
||||
wireguardPeerConfig = let
|
||||
snCfg = wgCfgOf serverNode;
|
||||
in {
|
||||
wireguardPeerConfig = {
|
||||
PublicKey = builtins.readFile (peerPublicKeyPath serverNode);
|
||||
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName serverNode}.path;
|
||||
# The allowed ips of a server node are it's own addreses,
|
||||
|
@ -117,7 +161,8 @@
|
|||
# ++ map (n: (wgCfgOf n).addresses) snCfg.ourClientNodes;
|
||||
Endpoint = "${snCfg.server.host}:${toString snCfg.server.port}";
|
||||
};
|
||||
}) (filterSelf associatedServerNodes)
|
||||
})
|
||||
(filterSelf associatedServerNodes)
|
||||
# All our external peers
|
||||
++ mapAttrsToList (extPeer: allowedIPs: let
|
||||
peerName = externalPeerName extPeer;
|
||||
|
@ -126,16 +171,22 @@
|
|||
PublicKey = builtins.readFile (peerPublicKeyPath peerName);
|
||||
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName peerName}.path;
|
||||
AllowedIPs = allowedIPs;
|
||||
# Connections to external peers should always be kept alive
|
||||
PersistentKeepalive = 25;
|
||||
};
|
||||
})
|
||||
wgCfg.server.externalPeers
|
||||
# All client nodes that have their via set to us.
|
||||
++ mapAttrsToList (clientNode: {
|
||||
wireguardPeerConfig = {
|
||||
++ mapAttrsToList (clientNode: let
|
||||
clientCfg = wgCfgOf clientNode;
|
||||
in {
|
||||
wireguardPeerConfig =
|
||||
{
|
||||
PublicKey = builtins.readFile (peerPublicKeyPath clientNode);
|
||||
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName clientNode}.path;
|
||||
AllowedIPs = (wgCfgOf clientNode).addresses;
|
||||
AllowedIPs = clientCfg.addresses;
|
||||
}
|
||||
// optionalAttrs clientCfg.keepalive {
|
||||
PersistentKeepalive = 25;
|
||||
};
|
||||
})
|
||||
|
@ -145,15 +196,16 @@
|
|||
[
|
||||
{
|
||||
wireguardPeerConfig = {
|
||||
PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.via);
|
||||
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName wgCfg.via}.path;
|
||||
AllowedIPs = (wgCfgOf wgCfg.via).addresses;
|
||||
PublicKey = builtins.readFile (peerPublicKeyPath wgCfg.client.via);
|
||||
PresharedKeyFile = config.rekey.secrets.${peerPresharedKeySecret nodeName wgCfg.client.via}.path;
|
||||
# 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;
|
||||
networkConfig.Address = wgCfg.addresses;
|
||||
};
|
||||
|
@ -162,14 +214,17 @@ in {
|
|||
options.extra.wireguard = mkOption {
|
||||
default = {};
|
||||
description = "Configures wireguard networks via systemd-networkd.";
|
||||
type = types.attrsOf (types.submodule {
|
||||
type = types.lazyAttrsOf (types.submodule ({
|
||||
config,
|
||||
name,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
server = {
|
||||
enable = mkEnableOption (mdDoc "wireguard server");
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
description = mdDoc "The hostname or ip address which other peers can use to reach this host.";
|
||||
default = null;
|
||||
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 {
|
||||
|
@ -181,11 +236,17 @@ in {
|
|||
openFirewall = mkOption {
|
||||
default = false;
|
||||
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 {
|
||||
type = types.attrsOf (types.listOf types.str);
|
||||
type = types.attrsOf (types.listOf (net.types.cidr-in config.addresses));
|
||||
default = {};
|
||||
example = {my-android-phone = ["10.0.0.97/32"];};
|
||||
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 {
|
||||
default = "20";
|
||||
type = types.str;
|
||||
default = 40;
|
||||
type = types.int;
|
||||
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 {
|
||||
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 ''
|
||||
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
|
||||
networkCfgs = mapAttrsToList configForNetwork cfg;
|
||||
collectAllNetworkAttrs = x: concatAttrs (map (y: y.${x}) networkCfgs);
|
||||
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";
|
||||
};
|
||||
});
|
||||
config = mkIf (cfg != {}) (mergeToplevelConfigs
|
||||
["assertions" "rekey" "networking" "systemd"]
|
||||
(mapAttrsToList configForNetwork cfg));
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
# plugin-files = ${pkgs.nix-plugins}/lib/nix/plugins
|
||||
# # Please adjust path accordingly, or leave this out and alternativaly
|
||||
# # pass `--option extra-builtins-file ./extra-builtins.nix` to each invocation
|
||||
# extra-builtins-file = ./extra-builtins.nix
|
||||
# extra-builtins-file = ${./extra-builtins.nix}
|
||||
# '';
|
||||
# }
|
||||
# ```
|
||||
|
|
10
nix/lib.nix
10
nix/lib.nix
|
@ -19,6 +19,7 @@
|
|||
head
|
||||
mapAttrs'
|
||||
mergeAttrs
|
||||
mkMerge
|
||||
nameValuePair
|
||||
optionalAttrs
|
||||
partition
|
||||
|
@ -50,6 +51,11 @@ in rec {
|
|||
# True if the path or string starts with /
|
||||
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 = {
|
||||
gpt = {
|
||||
partEfi = name: start: end: {
|
||||
|
@ -85,7 +91,7 @@ in rec {
|
|||
};
|
||||
};
|
||||
zfs = {
|
||||
encryptedZpool = {
|
||||
defaultZpoolOptions = {
|
||||
type = "zpool";
|
||||
mountRoot = "/mnt";
|
||||
rootFsOptions = {
|
||||
|
@ -164,7 +170,7 @@ in rec {
|
|||
# Partition nodes by whether they are servers
|
||||
_associatedNodes_isServerPartition =
|
||||
partition
|
||||
(n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.enable)
|
||||
(n: self.nodes.${n}.config.extra.wireguard.${wgName}.server.host != null)
|
||||
associatedNodes;
|
||||
|
||||
associatedServerNodes = _associatedNodes_isServerPartition.right;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue