From 80e7c1bdbf311b81a4b45fc9963be9c04d5a303b Mon Sep 17 00:00:00 2001 From: oddlama Date: Sat, 1 Jul 2023 01:11:58 +0200 Subject: [PATCH] refactor: finish decoupling the library functions from config --- hosts/nom/default.nix | 4 +- hosts/nom/fs.nix | 13 +- hosts/sentinel/fs.nix | 10 +- hosts/ward/default.nix | 29 +- hosts/ward/fs.nix | 9 +- .../default.nix => adguardhome.nix} | 0 .../{grafana/default.nix => grafana.nix} | 2 +- .../{influxdb/default.nix => influxdb.nix} | 0 .../{kanidm/default.nix => kanidm.nix} | 0 .../microvms/{loki/default.nix => loki.nix} | 0 .../default.nix => vaultwarden.nix} | 0 hosts/ward/net.nix | 6 +- .../secrets => secrets/adguardhome}/host.pub | 0 .../promtail-loki-basic-auth-password.age | 0 .../adguardhome}/telegraf-influxdb-token.age | 0 .../grafana-influxdb-basic-auth-password.age | 0 .../grafana}/grafana-influxdb-token.age | 0 .../grafana-loki-basic-auth-password.age | 0 .../grafana}/grafana-secret-key.age | 0 .../secrets => secrets/grafana}/host.pub | 0 .../promtail-loki-basic-auth-password.age | 0 .../grafana}/telegraf-influxdb-token.age | 0 .../admin-influxdb-basic-auth-password.age | 0 .../secrets => secrets/influxdb}/host.pub | 0 .../influxdb}/influxdb-basic-auth-hashes.age | Bin .../promtail-loki-basic-auth-password.age | Bin .../influxdb}/telegraf-influxdb-token.age | 0 .../secrets => secrets/kanidm}/host.pub | 0 .../kanidm}/kanidm-self-signed.crt.age | Bin .../kanidm}/kanidm-self-signed.key.age | Bin .../promtail-loki-basic-auth-password.age | Bin .../kanidm}/telegraf-influxdb-token.age | 0 .../loki/secrets => secrets/loki}/host.pub | 0 .../loki}/loki-basic-auth-hashes.age | Bin .../promtail-loki-basic-auth-password.age | 0 .../loki}/telegraf-influxdb-token.age | 0 .../secrets => secrets/vaultwarden}/host.pub | 0 .../promtail-loki-basic-auth-password.age | 0 .../vaultwarden}/telegraf-influxdb-token.age | 0 .../vaultwarden}/vaultwarden-env.age | Bin hosts/zackbiene/net.nix | 8 +- lib/disko.nix | 81 +++ lib/misc.nix | 119 ++++ lib/net.nix | 378 ++++++++++++ lib/types.nix | 74 +++ lib/wireguard.nix | 218 +++++++ modules/config/lib.nix | 560 ------------------ modules/default.nix | 1 - modules/meta/microvms.nix | 23 +- modules/meta/promtail.nix | 2 +- modules/meta/telegraf.nix | 2 +- modules/meta/wireguard.nix | 172 +----- modules/networking/interface-naming.nix | 8 +- modules/repo/distributed-config.nix | 36 +- nix/colmena.nix | 5 +- nix/generate-installer.nix | 2 +- nix/generate-node.nix | 2 +- users/myuser/default.nix | 3 +- users/root/default.nix | 3 +- 59 files changed, 984 insertions(+), 786 deletions(-) rename hosts/ward/microvms/{adguardhome/default.nix => adguardhome.nix} (100%) rename hosts/ward/microvms/{grafana/default.nix => grafana.nix} (98%) rename hosts/ward/microvms/{influxdb/default.nix => influxdb.nix} (100%) rename hosts/ward/microvms/{kanidm/default.nix => kanidm.nix} (100%) rename hosts/ward/microvms/{loki/default.nix => loki.nix} (100%) rename hosts/ward/microvms/{vaultwarden/default.nix => vaultwarden.nix} (100%) rename hosts/ward/{microvms/adguardhome/secrets => secrets/adguardhome}/host.pub (100%) rename hosts/ward/{microvms/adguardhome/secrets => secrets/adguardhome}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/adguardhome/secrets => secrets/adguardhome}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/grafana-influxdb-basic-auth-password.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/grafana-influxdb-token.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/grafana-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/grafana-secret-key.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/host.pub (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/grafana/secrets => secrets/grafana}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/influxdb/secrets => secrets/influxdb}/admin-influxdb-basic-auth-password.age (100%) rename hosts/ward/{microvms/influxdb/secrets => secrets/influxdb}/host.pub (100%) rename hosts/ward/{microvms/influxdb/secrets => secrets/influxdb}/influxdb-basic-auth-hashes.age (100%) rename hosts/ward/{microvms/influxdb/secrets => secrets/influxdb}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/influxdb/secrets => secrets/influxdb}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/kanidm/secrets => secrets/kanidm}/host.pub (100%) rename hosts/ward/{microvms/kanidm/secrets => secrets/kanidm}/kanidm-self-signed.crt.age (100%) rename hosts/ward/{microvms/kanidm/secrets => secrets/kanidm}/kanidm-self-signed.key.age (100%) rename hosts/ward/{microvms/kanidm/secrets => secrets/kanidm}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/kanidm/secrets => secrets/kanidm}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/loki/secrets => secrets/loki}/host.pub (100%) rename hosts/ward/{microvms/loki/secrets => secrets/loki}/loki-basic-auth-hashes.age (100%) rename hosts/ward/{microvms/loki/secrets => secrets/loki}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/loki/secrets => secrets/loki}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/vaultwarden/secrets => secrets/vaultwarden}/host.pub (100%) rename hosts/ward/{microvms/vaultwarden/secrets => secrets/vaultwarden}/promtail-loki-basic-auth-password.age (100%) rename hosts/ward/{microvms/vaultwarden/secrets => secrets/vaultwarden}/telegraf-influxdb-token.age (100%) rename hosts/ward/{microvms/vaultwarden/secrets => secrets/vaultwarden}/vaultwarden-env.age (100%) create mode 100644 lib/disko.nix create mode 100644 lib/misc.nix create mode 100644 lib/net.nix create mode 100644 lib/types.nix create mode 100644 lib/wireguard.nix delete mode 100644 modules/config/lib.nix diff --git a/hosts/nom/default.nix b/hosts/nom/default.nix index a416dd1..ee8229f 100644 --- a/hosts/nom/default.nix +++ b/hosts/nom/default.nix @@ -11,9 +11,7 @@ ../../modules/optional/hardware/intel.nix ../../modules/optional/hardware/physical.nix - #../../modules - ../../modules/config/lib.nix - + ../../modules ../../modules/optional/boot-efi.nix ../../modules/optional/initrd-ssh.nix ../../modules/optional/dev diff --git a/hosts/nom/fs.nix b/hosts/nom/fs.nix index 0a56396..eb72029 100644 --- a/hosts/nom/fs.nix +++ b/hosts/nom/fs.nix @@ -1,15 +1,16 @@ { + inputs, config, - lib, - pkgs, ... -}: { +}: let + disko = import ../../lib/disko.nix inputs; +in { disko.devices = { disk = { m2-ssd = { type = "disk"; device = "/dev/disk/by-id/${config.repo.secrets.local.disk.m2-ssd}"; - content = with config.lib.disko.gpt; { + content = with disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -20,7 +21,7 @@ boot-ssd = { type = "disk"; device = "/dev/disk/by-id/${config.repo.secrets.local.disk.boot-ssd}"; - content = with config.lib.disko.gpt; { + content = with disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -30,7 +31,7 @@ }; }; }; - zpool = with config.lib.disko.zfs; { + zpool = with disko.zfs; { rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;}; }; }; diff --git a/hosts/sentinel/fs.nix b/hosts/sentinel/fs.nix index 7c0db3d..1e2122a 100644 --- a/hosts/sentinel/fs.nix +++ b/hosts/sentinel/fs.nix @@ -1,14 +1,16 @@ { config, - pkgs, + inputs, ... -}: { +}: let + disko = import ../../lib/disko.nix inputs; +in { disko.devices = { disk = { main = { type = "disk"; device = "/dev/disk/by-id/${config.repo.secrets.local.disk.main}"; - content = with config.lib.disko.gpt; { + content = with disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -19,7 +21,7 @@ }; }; }; - zpool = with config.lib.disko.zfs; { + zpool = with disko.zfs; { rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;}; }; }; diff --git a/hosts/ward/default.nix b/hosts/ward/default.nix index d2244fc..fcc9371 100644 --- a/hosts/ward/default.nix +++ b/hosts/ward/default.nix @@ -1,6 +1,7 @@ { - inputs, config, + inputs, + lib, nodes, ... }: { @@ -43,29 +44,23 @@ ]; meta.microvms.vms = let - defaults = { + defaultConfig = name: { system = "x86_64-linux"; autostart = true; zfs = { enable = true; pool = "rpool"; }; - todo - configPath = - if nodePath != null && builtins.pathExists (nodePath + "/microvms/${name}") then - nodePath + "/microvms/${name}" - else if nodePath != null && builtins.pathExists (nodePath + "/microvms/${name}") then - nodePath + "/microvms/${name}.nix" - else null; + modules = [ + # XXX: this could be interpolated in-place but statix has a bug https://github.com/nerdypepper/statix/issues/75 + (./microvms + "/${name}.nix") + {node.secretsDir = ./secrets + "/${name}";} + ]; }; - in { - kanidm = defaults; - grafana = defaults; - loki = defaults; - vaultwarden = defaults; - adguardhome = defaults; - influxdb = defaults; - }; + in + lib.genAttrs + ["kanidm" "grafana" "loki" "vaultwarden" "adguardhome" "influxdb"] + defaultConfig; #ddclient = defineVm; #gitea/forgejo = defineVm; diff --git a/hosts/ward/fs.nix b/hosts/ward/fs.nix index 708325f..1d31b9f 100644 --- a/hosts/ward/fs.nix +++ b/hosts/ward/fs.nix @@ -1,15 +1,18 @@ { config, + inputs, lib, pkgs, ... -}: { +}: let + disko = import ../../lib/disko.nix inputs; +in { disko.devices = { disk = { m2-ssd = { type = "disk"; device = "/dev/disk/by-id/${config.repo.secrets.local.disk.m2-ssd}"; - content = with config.lib.disko.gpt; { + content = with disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -20,7 +23,7 @@ }; }; }; - zpool = with config.lib.disko.zfs; { + zpool = with disko.zfs; { rpool = defaultZpoolOptions // { diff --git a/hosts/ward/microvms/adguardhome/default.nix b/hosts/ward/microvms/adguardhome.nix similarity index 100% rename from hosts/ward/microvms/adguardhome/default.nix rename to hosts/ward/microvms/adguardhome.nix diff --git a/hosts/ward/microvms/grafana/default.nix b/hosts/ward/microvms/grafana.nix similarity index 98% rename from hosts/ward/microvms/grafana/default.nix rename to hosts/ward/microvms/grafana.nix index e069a36..374d5b0 100644 --- a/hosts/ward/microvms/grafana/default.nix +++ b/hosts/ward/microvms/grafana.nix @@ -122,7 +122,7 @@ in { url = "https://${sentinelCfg.networking.providedDomains.loki}"; orgId = 1; basicAuth = true; - basicAuthUser = "${config.repo.node.name}+grafana-loki-basic-auth-password"; + basicAuthUser = "${config.node.name}+grafana-loki-basic-auth-password"; secureJsonData.basicAuthPassword = "$__file{${config.age.secrets.grafana-loki-basic-auth-password.path}}"; } ]; diff --git a/hosts/ward/microvms/influxdb/default.nix b/hosts/ward/microvms/influxdb.nix similarity index 100% rename from hosts/ward/microvms/influxdb/default.nix rename to hosts/ward/microvms/influxdb.nix diff --git a/hosts/ward/microvms/kanidm/default.nix b/hosts/ward/microvms/kanidm.nix similarity index 100% rename from hosts/ward/microvms/kanidm/default.nix rename to hosts/ward/microvms/kanidm.nix diff --git a/hosts/ward/microvms/loki/default.nix b/hosts/ward/microvms/loki.nix similarity index 100% rename from hosts/ward/microvms/loki/default.nix rename to hosts/ward/microvms/loki.nix diff --git a/hosts/ward/microvms/vaultwarden/default.nix b/hosts/ward/microvms/vaultwarden.nix similarity index 100% rename from hosts/ward/microvms/vaultwarden/default.nix rename to hosts/ward/microvms/vaultwarden.nix diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index 2f82b60..e8667ee 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -1,10 +1,14 @@ { config, + inputs, lib, utils, ... }: let - inherit (config.lib.net) cidr; + inherit + (import ../../lib/net.nix inputs) + cidr + ; lanCidrv4 = "192.168.100.0/24"; lanCidrv6 = "fd10::/64"; diff --git a/hosts/ward/microvms/adguardhome/secrets/host.pub b/hosts/ward/secrets/adguardhome/host.pub similarity index 100% rename from hosts/ward/microvms/adguardhome/secrets/host.pub rename to hosts/ward/secrets/adguardhome/host.pub diff --git a/hosts/ward/microvms/adguardhome/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/adguardhome/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/adguardhome/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/adguardhome/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/adguardhome/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/adguardhome/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/adguardhome/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/adguardhome/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/grafana/secrets/grafana-influxdb-basic-auth-password.age b/hosts/ward/secrets/grafana/grafana-influxdb-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/grafana-influxdb-basic-auth-password.age rename to hosts/ward/secrets/grafana/grafana-influxdb-basic-auth-password.age diff --git a/hosts/ward/microvms/grafana/secrets/grafana-influxdb-token.age b/hosts/ward/secrets/grafana/grafana-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/grafana-influxdb-token.age rename to hosts/ward/secrets/grafana/grafana-influxdb-token.age diff --git a/hosts/ward/microvms/grafana/secrets/grafana-loki-basic-auth-password.age b/hosts/ward/secrets/grafana/grafana-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/grafana-loki-basic-auth-password.age rename to hosts/ward/secrets/grafana/grafana-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/grafana/secrets/grafana-secret-key.age b/hosts/ward/secrets/grafana/grafana-secret-key.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/grafana-secret-key.age rename to hosts/ward/secrets/grafana/grafana-secret-key.age diff --git a/hosts/ward/microvms/grafana/secrets/host.pub b/hosts/ward/secrets/grafana/host.pub similarity index 100% rename from hosts/ward/microvms/grafana/secrets/host.pub rename to hosts/ward/secrets/grafana/host.pub diff --git a/hosts/ward/microvms/grafana/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/grafana/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/grafana/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/grafana/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/grafana/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/grafana/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/grafana/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/influxdb/secrets/admin-influxdb-basic-auth-password.age b/hosts/ward/secrets/influxdb/admin-influxdb-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/influxdb/secrets/admin-influxdb-basic-auth-password.age rename to hosts/ward/secrets/influxdb/admin-influxdb-basic-auth-password.age diff --git a/hosts/ward/microvms/influxdb/secrets/host.pub b/hosts/ward/secrets/influxdb/host.pub similarity index 100% rename from hosts/ward/microvms/influxdb/secrets/host.pub rename to hosts/ward/secrets/influxdb/host.pub diff --git a/hosts/ward/microvms/influxdb/secrets/influxdb-basic-auth-hashes.age b/hosts/ward/secrets/influxdb/influxdb-basic-auth-hashes.age similarity index 100% rename from hosts/ward/microvms/influxdb/secrets/influxdb-basic-auth-hashes.age rename to hosts/ward/secrets/influxdb/influxdb-basic-auth-hashes.age diff --git a/hosts/ward/microvms/influxdb/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/influxdb/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/influxdb/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/influxdb/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/influxdb/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/influxdb/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/influxdb/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/influxdb/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/kanidm/secrets/host.pub b/hosts/ward/secrets/kanidm/host.pub similarity index 100% rename from hosts/ward/microvms/kanidm/secrets/host.pub rename to hosts/ward/secrets/kanidm/host.pub diff --git a/hosts/ward/microvms/kanidm/secrets/kanidm-self-signed.crt.age b/hosts/ward/secrets/kanidm/kanidm-self-signed.crt.age similarity index 100% rename from hosts/ward/microvms/kanidm/secrets/kanidm-self-signed.crt.age rename to hosts/ward/secrets/kanidm/kanidm-self-signed.crt.age diff --git a/hosts/ward/microvms/kanidm/secrets/kanidm-self-signed.key.age b/hosts/ward/secrets/kanidm/kanidm-self-signed.key.age similarity index 100% rename from hosts/ward/microvms/kanidm/secrets/kanidm-self-signed.key.age rename to hosts/ward/secrets/kanidm/kanidm-self-signed.key.age diff --git a/hosts/ward/microvms/kanidm/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/kanidm/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/kanidm/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/kanidm/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/kanidm/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/kanidm/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/kanidm/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/kanidm/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/loki/secrets/host.pub b/hosts/ward/secrets/loki/host.pub similarity index 100% rename from hosts/ward/microvms/loki/secrets/host.pub rename to hosts/ward/secrets/loki/host.pub diff --git a/hosts/ward/microvms/loki/secrets/loki-basic-auth-hashes.age b/hosts/ward/secrets/loki/loki-basic-auth-hashes.age similarity index 100% rename from hosts/ward/microvms/loki/secrets/loki-basic-auth-hashes.age rename to hosts/ward/secrets/loki/loki-basic-auth-hashes.age diff --git a/hosts/ward/microvms/loki/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/loki/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/loki/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/loki/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/loki/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/loki/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/loki/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/loki/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/vaultwarden/secrets/host.pub b/hosts/ward/secrets/vaultwarden/host.pub similarity index 100% rename from hosts/ward/microvms/vaultwarden/secrets/host.pub rename to hosts/ward/secrets/vaultwarden/host.pub diff --git a/hosts/ward/microvms/vaultwarden/secrets/promtail-loki-basic-auth-password.age b/hosts/ward/secrets/vaultwarden/promtail-loki-basic-auth-password.age similarity index 100% rename from hosts/ward/microvms/vaultwarden/secrets/promtail-loki-basic-auth-password.age rename to hosts/ward/secrets/vaultwarden/promtail-loki-basic-auth-password.age diff --git a/hosts/ward/microvms/vaultwarden/secrets/telegraf-influxdb-token.age b/hosts/ward/secrets/vaultwarden/telegraf-influxdb-token.age similarity index 100% rename from hosts/ward/microvms/vaultwarden/secrets/telegraf-influxdb-token.age rename to hosts/ward/secrets/vaultwarden/telegraf-influxdb-token.age diff --git a/hosts/ward/microvms/vaultwarden/secrets/vaultwarden-env.age b/hosts/ward/secrets/vaultwarden/vaultwarden-env.age similarity index 100% rename from hosts/ward/microvms/vaultwarden/secrets/vaultwarden-env.age rename to hosts/ward/secrets/vaultwarden/vaultwarden-env.age diff --git a/hosts/zackbiene/net.nix b/hosts/zackbiene/net.nix index 57d888f..3831e9e 100644 --- a/hosts/zackbiene/net.nix +++ b/hosts/zackbiene/net.nix @@ -1,9 +1,13 @@ { - lib, config, + inputs, + lib, ... }: let - inherit (config.lib.net) cidr; + inherit + (import ../../lib/net.nix inputs) + cidr + ; iotCidrv4 = "10.90.0.0/24"; iotCidrv6 = "fd00:90::/64"; diff --git a/lib/disko.nix b/lib/disko.nix new file mode 100644 index 0000000..a6d8b80 --- /dev/null +++ b/lib/disko.nix @@ -0,0 +1,81 @@ +inputs: { + gpt = { + partGrub = name: start: end: { + inherit name start end; + part-type = "primary"; + flags = ["bios_grub"]; + }; + partEfi = name: start: end: { + inherit name start end; + fs-type = "fat32"; + bootable = true; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + partSwap = name: start: end: { + inherit name start end; + fs-type = "linux-swap"; + content = { + type = "swap"; + randomEncryption = true; + }; + }; + partLuksZfs = name: start: end: { + inherit start end; + name = "enc-${name}"; + content = { + type = "luks"; + name = "enc-${name}"; + extraOpenArgs = ["--allow-discards"]; + content = { + type = "zfs"; + pool = name; + }; + }; + }; + }; + zfs = rec { + defaultZpoolOptions = { + type = "zpool"; + mountRoot = "/mnt"; + rootFsOptions = { + compression = "zstd"; + acltype = "posix"; + atime = "off"; + xattr = "sa"; + dnodesize = "auto"; + mountpoint = "none"; + canmount = "off"; + devices = "off"; + }; + options.ashift = "12"; + }; + + defaultZfsDatasets = { + "local" = unmountable; + "local/root" = + filesystem "/" + // { + postCreateHook = "zfs snapshot rpool/local/root@blank"; + }; + "local/nix" = filesystem "/nix"; + "local/state" = filesystem "/state"; + "safe" = unmountable; + "safe/persist" = filesystem "/persist"; + }; + + unmountable = {type = "zfs_fs";}; + filesystem = mountpoint: { + type = "zfs_fs"; + options = { + canmount = "noauto"; + inherit mountpoint; + }; + # Required to add dependencies for initrd + inherit mountpoint; + }; + }; +} diff --git a/lib/misc.nix b/lib/misc.nix new file mode 100644 index 0000000..3a60cc4 --- /dev/null +++ b/lib/misc.nix @@ -0,0 +1,119 @@ +inputs: let + inherit + (inputs.nixpkgs.lib) + all + any + assertMsg + attrNames + attrValues + concatLists + concatMap + concatMapStrings + concatStringsSep + elem + escapeShellArg + filter + flatten + flip + foldAttrs + foldl' + genAttrs + genList + hasInfix + head + isAttrs + mapAttrs' + mergeAttrs + min + mkMerge + mkOptionType + nameValuePair + optionalAttrs + partition + range + recursiveUpdate + removeSuffix + reverseList + showOption + splitString + stringToCharacters + substring + types + unique + warnIf + ; +in rec { + # Counts how often each element occurrs in xs + countOccurrences = let + addOrUpdate = acc: x: + acc // {${x} = (acc.${x} or 0) + 1;}; + in + foldl' addOrUpdate {}; + + # Returns all elements in xs that occur at least twice + duplicates = xs: let + occurrences = countOccurrences xs; + in + unique (filter (x: occurrences.${x} > 1) xs); + + # Concatenates all given attrsets as if calling a // b in order. + concatAttrs = foldl' mergeAttrs {}; + + # 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)); + + # Calculates base^exp, but careful, this overflows for results > 2^62 + pow = base: exp: foldl' (a: x: x * a) 1 (genList (_: base) exp); + + # Converts the given hex string to an integer. Only reliable for inputs in [0, 2^63), + # after that the sign bit will overflow. + hexToDec = v: let + literalValues = { + "0" = 0; + "1" = 1; + "2" = 2; + "3" = 3; + "4" = 4; + "5" = 5; + "6" = 6; + "7" = 7; + "8" = 8; + "9" = 9; + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + "A" = 10; + "B" = 11; + "C" = 12; + "D" = 13; + "E" = 14; + "F" = 15; + }; + in + foldl' (acc: x: acc * 16 + literalValues.${x}) 0 (stringToCharacters v); + + secrets = let + rageMasterIdentityArgs = concatMapStrings (x: "-i ${escapeShellArg x} ") inputs.self.secretsConfig.masterIdentities; + rageExtraEncryptionPubkeys = + concatMapStrings ( + x: + if isAbsolutePath x + then "-R ${escapeShellArg x} " + else "-r ${escapeShellArg x} " + ) + inputs.self.secretsConfig.extraEncryptionPubkeys; + in { + # TODO replace these by lib.agenix-rekey + # The arguments required to de-/encrypt a secret in this repository + rageDecryptArgs = "${rageMasterIdentityArgs}"; + rageEncryptArgs = "${rageMasterIdentityArgs} ${rageExtraEncryptionPubkeys}"; + }; +} diff --git a/lib/net.nix b/lib/net.nix new file mode 100644 index 0000000..849ed70 --- /dev/null +++ b/lib/net.nix @@ -0,0 +1,378 @@ +inputs: let + inherit + (inputs.nixpkgs.lib) + all + any + assertMsg + attrNames + attrValues + concatLists + concatMap + concatMapStrings + concatStringsSep + elem + escapeShellArg + filter + flatten + flip + foldAttrs + foldl' + genAttrs + genList + hasInfix + head + isAttrs + mapAttrs' + mergeAttrs + min + mkMerge + mkOptionType + nameValuePair + optionalAttrs + partition + range + recursiveUpdate + removeSuffix + reverseList + showOption + splitString + stringToCharacters + substring + types + unique + warnIf + ; + + inherit + (import ./misc.nix inputs) + hexToDec + pow + ; + + # IP address math library + # https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba + # Plus some extensions by us + libNet = + (import "${inputs.lib-net}/net.nix" { + inherit (inputs.nixpkgs) lib; + }) + .lib + .net; +in + recursiveUpdate libNet { + 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" + # + # > 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" + # + host = i: n: let + cap = libNet.cidr.capacity n; + in + assert assertMsg (i >= (-cap) && i < cap) "The host ${toString i} lies outside of ${n}"; + libNet.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: "${libNet.cidr.host n x}/${toString (libNet.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: head (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: libNet.cidr.make (libNet.cidr.length x) (ip x); + # mergev4 :: [cidrv4 | ipv4] -> (cidrv4 | null) + # + # Returns the smallest cidr network that includes all given networks. + # If no cidr mask is given, /32 is assumed. + # + # Examples: + # + # > net.cidr.mergev4 ["192.168.1.1/24" "192.168.6.1/32"] + # "192.168.0.0/21" + mergev4 = addrs_: let + # Append /32 if necessary + addrs = map (x: + if hasInfix "/" x + then x + else "${x}/32") + addrs_; + # 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 = foldl' min 32 (map libNet.cidr.length addrs); + possibleLengths = reverseList (range 0 startLength); + # The first ip address will be "expanded" in cidr length until it covers all other + # used addresses. + firstIp = ip (head addrs); + # Return the first (i.e. greatest length -> smallest prefix) cidr length + # in the list that covers all used addresses + bestLength = head (filter + # All given addresses must be contained by the generated address. + (len: + all (x: + libNet.cidr.contains + (ip x) + (libNet.cidr.make len firstIp)) + addrs) + possibleLengths); + in + assert assertMsg (!any (hasInfix ":") addrs) "mergev4 cannot operate on ipv6 addresses"; + if addrs == [] + then null + else libNet.cidr.make bestLength firstIp; + # mergev6 :: [cidrv6 | ipv6] -> (cidrv6 | null) + # + # Returns the smallest cidr network that includes all given networks. + # If no cidr mask is given, /128 is assumed. + # + # Examples: + # + # > net.cidr.mergev6 ["fd00:dead:cafe::/64" "fd00:fd12:3456:7890::/56"] + # "fd00:c000::/18" + mergev6 = addrs_: let + # Append /128 if necessary + addrs = map (x: + if hasInfix "/" x + then x + else "${x}/128") + addrs_; + # 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 = foldl' min 128 (map libNet.cidr.length addrs); + possibleLengths = reverseList (range 0 startLength); + # The first ip address will be "expanded" in cidr length until it covers all other + # used addresses. + firstIp = ip (head addrs); + # Return the first (i.e. greatest length -> smallest prefix) cidr length + # in the list that covers all used addresses + bestLength = head (filter + # All given addresses must be contained by the generated address. + (len: + all (x: + libNet.cidr.contains + (ip x) + (libNet.cidr.make len firstIp)) + addrs) + possibleLengths); + in + assert assertMsg (all (hasInfix ":") addrs) "mergev6 cannot operate on ipv4 addresses"; + if addrs == [] + then null + else libNet.cidr.make bestLength firstIp; + # merge :: [cidr] -> { cidrv4 = (cidrv4 | null); cidrv6 = (cidrv4 | null); } + # + # Returns the smallest cidr network that includes all given networks, + # but yields two separate result for all given ipv4 and ipv6 addresses. + # Equivalent to calling mergev4 and mergev6 on a partition individually. + merge = addrs: let + v4_and_v6 = partition (hasInfix ":") addrs; + in { + cidrv4 = mergev4 v4_and_v6.wrong; + cidrv6 = mergev6 v4_and_v6.right; + }; + # assignIps :: cidr -> [int | ip] -> [string] -> [ip] + # + # Assigns a semi-stable ip address from the given cidr network to each hostname. + # The algorithm is based on hashing (abusing sha256) with linear probing. + # The order of hosts doesn't matter. No ip (or offset) from the reserved list + # will be assigned. The network address and broadcast address will always be reserved + # automatically. + # + # Examples: + # + # > net.cidr.assignIps "192.168.100.1/24" [] ["a" "b" "c"] + # { a = "192.168.100.202"; b = "192.168.100.74"; c = "192.168.100.226"; } + # + # > net.cidr.assignIps "192.168.100.1/24" [] ["a" "b" "c" "a-new-elem"] + # { a = "192.168.100.202"; a-new-elem = "192.168.100.88"; b = "192.168.100.74"; c = "192.168.100.226"; } + # + # > net.cidr.assignIps "192.168.100.1/24" [202 "192.168.100.74"] ["a" "b" "c"] + # { a = "192.168.100.203"; b = "192.168.100.75"; c = "192.168.100.226"; } + assignIps = net: reserved: hosts: let + cidrSize = libNet.cidr.size net; + capacity = libNet.cidr.capacity net; + # The base address of the network. Used to convert ip-based reservations to offsets + baseAddr = host 0 net; + # Reserve some values for the network, host and broadcast address. + # The network and broadcast address should never be used, and we + # want to reserve the host address for the host. We also convert + # any ips to offsets here. + init = unique ( + [0 (capacity - 1)] + ++ flip map reserved (x: + if builtins.typeOf x == "int" + then x + else -(libNet.ip.diff baseAddr x)) + ); + nHosts = builtins.length hosts; + nInit = builtins.length init; + # Pre-sort all hosts, to ensure ordering invariance + sortedHosts = + warnIf + ((nInit + nHosts) > 0.3 * capacity) + "assignIps: hash stability may be degraded since utilization is >30%" + (builtins.sort builtins.lessThan hosts); + # Generates a hash (i.e. offset value) for a given hostname + hashElem = x: + builtins.bitAnd (capacity - 1) + (hexToDec (builtins.substring 0 16 (builtins.hashString "sha256" x))); + # Do linear probing. Returns the first unused value at or after the given value. + probe = avoid: value: + if elem value avoid + # TODO lib.mod + # Poor man's modulo, because nix has no modulo. Luckily we operate on a residue + # class of x modulo 2^n, so we can use bitAnd instead. + then probe avoid (builtins.bitAnd (capacity - 1) (value + 1)) + else value; + # Hash a new element and avoid assigning any existing values. + assignOne = { + assigned, + used, + }: x: let + value = probe used (hashElem x); + in { + assigned = + assigned + // { + ${x} = host value net; + }; + used = [value] ++ used; + }; + in + assert assertMsg (cidrSize >= 2 && cidrSize <= 62) + "assignIps: cidrSize=${toString cidrSize} is not in [2, 62]."; + assert 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 + (foldl' assignOne { + assigned = {}; + used = init; + } + sortedHosts) + .assigned; + }; + 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 = hasInfix ":"; + }; + mac = { + # Adds offset to the given base address and ensures the result is in + # a locally administered range by replacing the second nibble with a 2. + addPrivate = base: offset: let + added = libNet.mac.add base offset; + pre = substring 0 1 added; + suf = substring 2 (-1) added; + in "${pre}2${suf}"; + # assignMacs :: mac (base) -> int (size) -> [int | mac] (reserved) -> [string] (hosts) -> [mac] + # + # Assigns a semi-stable MAC address starting in [base, base + 2^size) to each hostname. + # The algorithm is based on hashing (abusing sha256) with linear probing. + # The order of hosts doesn't matter. No mac (or offset) from the reserved list + # will be assigned. + # + # Examples: + # + # > net.mac.assignMacs "11:22:33:00:00:00" 24 [] ["a" "b" "c"] + # { a = "11:22:33:1b:bd:ca"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } + # + # > net.mac.assignMacs "11:22:33:00:00:00" 24 [] ["a" "b" "c" "a-new-elem"] + # { a = "11:22:33:1b:bd:ca"; a-new-elem = "11:22:33:d6:5d:58"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } + # + # > net.mac.assignMacs "11:22:33:00:00:00" 24 ["11:22:33:1b:bd:ca"] ["a" "b" "c"] + # { a = "11:22:33:1b:bd:cb"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } + assignMacs = base: size: reserved: hosts: let + capacity = pow 2 size; + baseAsInt = libNet.mac.diff base "00:00:00:00:00:00"; + init = unique ( + flip map reserved (x: + if builtins.typeOf x == "int" + then x + else libNet.mac.diff x base) + ); + nHosts = builtins.length hosts; + nInit = builtins.length init; + # Pre-sort all hosts, to ensure ordering invariance + sortedHosts = + warnIf + ((nInit + nHosts) > 0.3 * capacity) + "assignMacs: hash stability may be degraded since utilization is >30%" + (builtins.sort builtins.lessThan hosts); + # Generates a hash (i.e. offset value) for a given hostname + hashElem = x: + builtins.bitAnd (capacity - 1) + (hexToDec (builtins.substring 0 16 (builtins.hashString "sha256" x))); + # Do linear probing. Returns the first unused value at or after the given value. + probe = avoid: value: + if elem value avoid + # TODO lib.mod + # Poor man's modulo, because nix has no modulo. Luckily we operate on a residue + # class of x modulo 2^n, so we can use bitAnd instead. + then probe avoid (builtins.bitAnd (capacity - 1) (value + 1)) + else value; + # Hash a new element and avoid assigning any existing values. + assignOne = { + assigned, + used, + }: x: let + value = probe used (hashElem x); + in { + assigned = + assigned + // { + ${x} = libNet.mac.add value base; + }; + used = [value] ++ used; + }; + in + assert assertMsg (size >= 2 && size <= 62) + "assignMacs: size=${toString size} is not in [2, 62]."; + assert assertMsg (builtins.bitAnd (capacity - 1) baseAsInt == 0) + "assignMacs: the size=${toString size} least significant bits of the base mac address must be 0."; + assert assertMsg (nHosts <= capacity - nInit) + "assignMacs: number of hosts (${toString nHosts}) must be <= capacity (${toString capacity}) - reserved (${toString nInit})"; + # Assign an ip in the subnet to each element, in order + (foldl' assignOne { + assigned = {}; + used = init; + } + sortedHosts) + .assigned; + }; + } diff --git a/lib/types.nix b/lib/types.nix new file mode 100644 index 0000000..cc19775 --- /dev/null +++ b/lib/types.nix @@ -0,0 +1,74 @@ +inputs: let + inherit + (inputs.nixpkgs.lib) + all + any + assertMsg + attrNames + attrValues + concatLists + concatMap + concatMapStrings + concatStringsSep + elem + escapeShellArg + filter + flatten + flip + foldAttrs + foldl' + genAttrs + genList + hasInfix + head + isAttrs + mapAttrs' + mergeAttrs + min + mkMerge + mkOptionType + nameValuePair + optionalAttrs + partition + range + recursiveUpdate + removeSuffix + reverseList + showOption + splitString + stringToCharacters + substring + types + unique + warnIf + ; +in 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 = ; }. + # 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"; + types.mergeOneOption loc defs; + substSubModules = m: types.uniq (type.substSubModules m); + functor = (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: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type; +} diff --git a/lib/wireguard.nix b/lib/wireguard.nix new file mode 100644 index 0000000..18d6a8d --- /dev/null +++ b/lib/wireguard.nix @@ -0,0 +1,218 @@ +inputs: wgName: let + inherit + (inputs.nixpkgs.lib) + all + any + assertMsg + attrNames + attrValues + concatLists + concatMap + concatMapStrings + concatStringsSep + elem + escapeShellArg + filter + flatten + flip + foldAttrs + foldl' + genAttrs + genList + hasInfix + head + isAttrs + mapAttrs' + mergeAttrs + min + mkMerge + mkOptionType + nameValuePair + optionalAttrs + partition + range + recursiveUpdate + removeSuffix + reverseList + showOption + splitString + stringToCharacters + substring + types + unique + warnIf + ; + + net = import ./net.nix inputs; + misc = import ./misc.nix inputs; + inherit + (import ./types.nix inputs) + isLazyValue + ; + + inherit + (misc) + concatAttrs + ; + + inherit + (misc.secrets) + rageDecryptArgs + ; + + inherit (inputs.self) nodes; +in rec { + # Returns the given node's wireguard configuration of this network + wgCfgOf = node: nodes.${node}.config.meta.wireguard.${wgName}; + + sortedPeers = peerA: peerB: + if peerA < peerB + then { + peer1 = peerA; + peer2 = peerB; + } + else { + peer1 = peerB; + peer2 = peerA; + }; + + peerPublicKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.pub"; + peerPublicKeyPath = peerName: inputs.self.outPath + peerPublicKeyFile peerName; + + peerPrivateKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.age"; + peerPrivateKeyPath = peerName: inputs.self.outPath + peerPrivateKeyFile peerName; + peerPrivateKeySecret = peerName: "wireguard-${wgName}-priv-${peerName}"; + + peerPresharedKeyFile = peerA: peerB: let + inherit (sortedPeers peerA peerB) peer1 peer2; + in "/secrets/wireguard/${wgName}/psks/${peer1}+${peer2}.age"; + peerPresharedKeyPath = peerA: peerB: inputs.self.outPath + peerPresharedKeyFile peerA peerB; + peerPresharedKeySecret = peerA: peerB: let + inherit (sortedPeers peerA peerB) peer1 peer2; + in "wireguard-${wgName}-psks-${peer1}+${peer2}"; + + # All nodes that are part of this network + participatingNodes = + filter + (n: builtins.hasAttr wgName nodes.${n}.config.meta.wireguard) + (attrNames nodes); + + # Partition nodes by whether they are servers + _participatingNodes_isServerPartition = + partition + (n: (wgCfgOf n).server.host != null) + participatingNodes; + + participatingServerNodes = _participatingNodes_isServerPartition.right; + participatingClientNodes = _participatingNodes_isServerPartition.wrong; + + # Maps all nodes that are part of this network to their addresses + nodePeers = genAttrs participatingNodes (n: (wgCfgOf n).addresses); + + externalPeerName = p: "external-${p}"; + + # Only peers that are defined as externalPeers on the given node. + # Prepends "external-" to their name. + externalPeersForNode = node: + mapAttrs' (p: nameValuePair (externalPeerName p)) (wgCfgOf node).server.externalPeers; + + # All peers that are defined as externalPeers on any node. + # Prepends "external-" to their name. + 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) participatingNodes; + + # A list of all occurring addresses. + usedAddresses = + concatMap (n: (wgCfgOf n).addresses) participatingNodes + ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); + + # A list of all occurring addresses, but only includes addresses that + # are not assigned automatically. + explicitlyUsedAddresses = + flip concatMap participatingNodes + (n: + filter (x: !isLazyValue x) + (concatLists + (nodes.${n}.options.meta.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) 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 + 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 + # storing them in the nix-store. + wgQuickConfigScript = system: serverNode: extPeer: let + pkgs = inputs.self.pkgs.${system}; + snCfg = wgCfgOf serverNode; + 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)}) \ + || { echo "error: Failed to decrypt!" >&2; exit 1; } + serverPsk=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPresharedKeyPath serverNode peerName)}) \ + || { echo "error: Failed to decrypt!" >&2; exit 1; } + + cat <; }. - # 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"; - types.mergeOneOption loc defs; - substSubModules = m: types.uniq (type.substSubModules m); - functor = (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: types.coercedTo (lazyValueOf type) (x: x._lazyValue) type; - }; - misc = rec { - # Counts how often each element occurrs in xs - countOccurrences = let - addOrUpdate = acc: x: - acc // {${x} = (acc.${x} or 0) + 1;}; - in - foldl' addOrUpdate {}; - - # Returns all elements in xs that occur at least twice - duplicates = xs: let - occurrences = countOccurrences xs; - in - unique (filter (x: occurrences.${x} > 1) xs); - - # Concatenates all given attrsets as if calling a // b in order. - concatAttrs = foldl' mergeAttrs {}; - - # 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)); - - # Calculates base^exp, but careful, this overflows for results > 2^62 - pow = base: exp: foldl' (a: x: x * a) 1 (genList (_: base) exp); - - # Converts the given hex string to an integer. Only reliable for inputs in [0, 2^63), - # after that the sign bit will overflow. - hexToDec = v: let - literalValues = { - "0" = 0; - "1" = 1; - "2" = 2; - "3" = 3; - "4" = 4; - "5" = 5; - "6" = 6; - "7" = 7; - "8" = 8; - "9" = 9; - "a" = 10; - "b" = 11; - "c" = 12; - "d" = 13; - "e" = 14; - "f" = 15; - "A" = 10; - "B" = 11; - "C" = 12; - "D" = 13; - "E" = 14; - "F" = 15; - }; - in - foldl' (acc: x: acc * 16 + literalValues.${x}) 0 (stringToCharacters v); - }; - disko = { - gpt = { - partGrub = name: start: end: { - inherit name start end; - part-type = "primary"; - flags = ["bios_grub"]; - }; - partEfi = name: start: end: { - inherit name start end; - fs-type = "fat32"; - bootable = true; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - }; - }; - partSwap = name: start: end: { - inherit name start end; - fs-type = "linux-swap"; - content = { - type = "swap"; - randomEncryption = true; - }; - }; - partLuksZfs = name: start: end: { - inherit start end; - name = "enc-${name}"; - content = { - type = "luks"; - name = "enc-${name}"; - extraOpenArgs = ["--allow-discards"]; - content = { - type = "zfs"; - pool = name; - }; - }; - }; - }; - zfs = rec { - defaultZpoolOptions = { - type = "zpool"; - mountRoot = "/mnt"; - rootFsOptions = { - compression = "zstd"; - acltype = "posix"; - atime = "off"; - xattr = "sa"; - dnodesize = "auto"; - mountpoint = "none"; - canmount = "off"; - devices = "off"; - }; - options.ashift = "12"; - }; - - defaultZfsDatasets = { - "local" = unmountable; - "local/root" = - filesystem "/" - // { - postCreateHook = "zfs snapshot rpool/local/root@blank"; - }; - "local/nix" = filesystem "/nix"; - "local/state" = filesystem "/state"; - "safe" = unmountable; - "safe/persist" = filesystem "/persist"; - }; - - unmountable = {type = "zfs_fs";}; - filesystem = mountpoint: { - type = "zfs_fs"; - options = { - canmount = "noauto"; - inherit mountpoint; - }; - # Required to add dependencies for initrd - inherit mountpoint; - }; - }; - }; - secrets = let - rageMasterIdentityArgs = concatMapStrings (x: "-i ${escapeShellArg x} ") inputs.self.secretsConfig.masterIdentities; - rageExtraEncryptionPubkeys = - concatMapStrings ( - x: - if misc.isAbsolutePath x - then "-R ${escapeShellArg x} " - else "-r ${escapeShellArg x} " - ) - inputs.self.secretsConfig.extraEncryptionPubkeys; - in { - # TODO replace these by lib.agenix-rekey - # The arguments required to de-/encrypt a secret in this repository - rageDecryptArgs = "${rageMasterIdentityArgs}"; - rageEncryptArgs = "${rageMasterIdentityArgs} ${rageExtraEncryptionPubkeys}"; - }; - 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" - # - # > 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" - # - host = i: n: let - cap = libWithNet.net.cidr.capacity n; - in - assert 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: head (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); - # mergev4 :: [cidrv4 | ipv4] -> (cidrv4 | null) - # - # Returns the smallest cidr network that includes all given networks. - # If no cidr mask is given, /32 is assumed. - # - # Examples: - # - # > net.cidr.mergev4 ["192.168.1.1/24" "192.168.6.1/32"] - # "192.168.0.0/21" - mergev4 = addrs_: let - # Append /32 if necessary - addrs = map (x: - if hasInfix "/" x - then x - else "${x}/32") - addrs_; - # 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 = foldl' min 32 (map libWithNet.net.cidr.length addrs); - possibleLengths = reverseList (range 0 startLength); - # The first ip address will be "expanded" in cidr length until it covers all other - # used addresses. - firstIp = ip (head addrs); - # Return the first (i.e. greatest length -> smallest prefix) cidr length - # in the list that covers all used addresses - bestLength = head (filter - # All given addresses must be contained by the generated address. - (len: - all (x: - libWithNet.net.cidr.contains - (ip x) - (libWithNet.net.cidr.make len firstIp)) - addrs) - possibleLengths); - in - assert assertMsg (!any (hasInfix ":") addrs) "mergev4 cannot operate on ipv6 addresses"; - if addrs == [] - then null - else libWithNet.net.cidr.make bestLength firstIp; - # mergev6 :: [cidrv6 | ipv6] -> (cidrv6 | null) - # - # Returns the smallest cidr network that includes all given networks. - # If no cidr mask is given, /128 is assumed. - # - # Examples: - # - # > net.cidr.mergev6 ["fd00:dead:cafe::/64" "fd00:fd12:3456:7890::/56"] - # "fd00:c000::/18" - mergev6 = addrs_: let - # Append /128 if necessary - addrs = map (x: - if hasInfix "/" x - then x - else "${x}/128") - addrs_; - # 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 = foldl' min 128 (map libWithNet.net.cidr.length addrs); - possibleLengths = reverseList (range 0 startLength); - # The first ip address will be "expanded" in cidr length until it covers all other - # used addresses. - firstIp = ip (head addrs); - # Return the first (i.e. greatest length -> smallest prefix) cidr length - # in the list that covers all used addresses - bestLength = head (filter - # All given addresses must be contained by the generated address. - (len: - all (x: - libWithNet.net.cidr.contains - (ip x) - (libWithNet.net.cidr.make len firstIp)) - addrs) - possibleLengths); - in - assert assertMsg (all (hasInfix ":") addrs) "mergev6 cannot operate on ipv4 addresses"; - if addrs == [] - then null - else libWithNet.net.cidr.make bestLength firstIp; - # merge :: [cidr] -> { cidrv4 = (cidrv4 | null); cidrv6 = (cidrv4 | null); } - # - # Returns the smallest cidr network that includes all given networks, - # but yields two separate result for all given ipv4 and ipv6 addresses. - # Equivalent to calling mergev4 and mergev6 on a partition individually. - merge = addrs: let - v4_and_v6 = partition (hasInfix ":") addrs; - in { - cidrv4 = mergev4 v4_and_v6.wrong; - cidrv6 = mergev6 v4_and_v6.right; - }; - # assignIps :: cidr -> [int | ip] -> [string] -> [ip] - # - # Assigns a semi-stable ip address from the given cidr network to each hostname. - # The algorithm is based on hashing (abusing sha256) with linear probing. - # The order of hosts doesn't matter. No ip (or offset) from the reserved list - # will be assigned. The network address and broadcast address will always be reserved - # automatically. - # - # Examples: - # - # > net.cidr.assignIps "192.168.100.1/24" [] ["a" "b" "c"] - # { a = "192.168.100.202"; b = "192.168.100.74"; c = "192.168.100.226"; } - # - # > net.cidr.assignIps "192.168.100.1/24" [] ["a" "b" "c" "a-new-elem"] - # { a = "192.168.100.202"; a-new-elem = "192.168.100.88"; b = "192.168.100.74"; c = "192.168.100.226"; } - # - # > net.cidr.assignIps "192.168.100.1/24" [202 "192.168.100.74"] ["a" "b" "c"] - # { a = "192.168.100.203"; b = "192.168.100.75"; c = "192.168.100.226"; } - assignIps = net: reserved: hosts: let - cidrSize = libWithNet.net.cidr.size net; - capacity = libWithNet.net.cidr.capacity net; - # The base address of the network. Used to convert ip-based reservations to offsets - baseAddr = host 0 net; - # Reserve some values for the network, host and broadcast address. - # The network and broadcast address should never be used, and we - # want to reserve the host address for the host. We also convert - # any ips to offsets here. - init = unique ( - [0 (capacity - 1)] - ++ flip map reserved (x: - if builtins.typeOf x == "int" - then x - else -(libWithNet.net.ip.diff baseAddr x)) - ); - nHosts = builtins.length hosts; - nInit = builtins.length init; - # Pre-sort all hosts, to ensure ordering invariance - sortedHosts = - warnIf - ((nInit + nHosts) > 0.3 * capacity) - "assignIps: hash stability may be degraded since utilization is >30%" - (builtins.sort builtins.lessThan hosts); - # Generates a hash (i.e. offset value) for a given hostname - hashElem = x: - builtins.bitAnd (capacity - 1) - (misc.hexToDec (builtins.substring 0 16 (builtins.hashString "sha256" x))); - # Do linear probing. Returns the first unused value at or after the given value. - probe = avoid: value: - if elem value avoid - # TODO lib.mod - # Poor man's modulo, because nix has no modulo. Luckily we operate on a residue - # class of x modulo 2^n, so we can use bitAnd instead. - then probe avoid (builtins.bitAnd (capacity - 1) (value + 1)) - else value; - # Hash a new element and avoid assigning any existing values. - assignOne = { - assigned, - used, - }: x: let - value = probe used (hashElem x); - in { - assigned = - assigned - // { - ${x} = host value net; - }; - used = [value] ++ used; - }; - in - assert assertMsg (cidrSize >= 2 && cidrSize <= 62) - "assignIps: cidrSize=${toString cidrSize} is not in [2, 62]."; - assert 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 - (foldl' assignOne { - assigned = {}; - used = init; - } - sortedHosts) - .assigned; - }; - 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 = hasInfix ":"; - }; - mac = { - # Adds offset to the given base address and ensures the result is in - # a locally administered range by replacing the second nibble with a 2. - addPrivate = base: offset: let - added = libWithNet.net.mac.add base offset; - pre = substring 0 1 added; - suf = substring 2 (-1) added; - in "${pre}2${suf}"; - # assignMacs :: mac (base) -> int (size) -> [int | mac] (reserved) -> [string] (hosts) -> [mac] - # - # Assigns a semi-stable MAC address starting in [base, base + 2^size) to each hostname. - # The algorithm is based on hashing (abusing sha256) with linear probing. - # The order of hosts doesn't matter. No mac (or offset) from the reserved list - # will be assigned. - # - # Examples: - # - # > net.mac.assignMacs "11:22:33:00:00:00" 24 [] ["a" "b" "c"] - # { a = "11:22:33:1b:bd:ca"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } - # - # > net.mac.assignMacs "11:22:33:00:00:00" 24 [] ["a" "b" "c" "a-new-elem"] - # { a = "11:22:33:1b:bd:ca"; a-new-elem = "11:22:33:d6:5d:58"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } - # - # > net.mac.assignMacs "11:22:33:00:00:00" 24 ["11:22:33:1b:bd:ca"] ["a" "b" "c"] - # { a = "11:22:33:1b:bd:cb"; b = "11:22:33:39:59:4a"; c = "11:22:33:50:7a:e2"; } - assignMacs = base: size: reserved: hosts: let - capacity = misc.pow 2 size; - baseAsInt = libWithNet.net.mac.diff base "00:00:00:00:00:00"; - init = unique ( - flip map reserved (x: - if builtins.typeOf x == "int" - then x - else libWithNet.net.mac.diff x base) - ); - nHosts = builtins.length hosts; - nInit = builtins.length init; - # Pre-sort all hosts, to ensure ordering invariance - sortedHosts = - warnIf - ((nInit + nHosts) > 0.3 * capacity) - "assignMacs: hash stability may be degraded since utilization is >30%" - (builtins.sort builtins.lessThan hosts); - # Generates a hash (i.e. offset value) for a given hostname - hashElem = x: - builtins.bitAnd (capacity - 1) - (misc.hexToDec (builtins.substring 0 16 (builtins.hashString "sha256" x))); - # Do linear probing. Returns the first unused value at or after the given value. - probe = avoid: value: - if elem value avoid - # TODO lib.mod - # Poor man's modulo, because nix has no modulo. Luckily we operate on a residue - # class of x modulo 2^n, so we can use bitAnd instead. - then probe avoid (builtins.bitAnd (capacity - 1) (value + 1)) - else value; - # Hash a new element and avoid assigning any existing values. - assignOne = { - assigned, - used, - }: x: let - value = probe used (hashElem x); - in { - assigned = - assigned - // { - ${x} = libWithNet.net.mac.add value base; - }; - used = [value] ++ used; - }; - in - assert assertMsg (size >= 2 && size <= 62) - "assignMacs: size=${toString size} is not in [2, 62]."; - assert assertMsg (builtins.bitAnd (capacity - 1) baseAsInt == 0) - "assignMacs: the size=${toString size} least significant bits of the base mac address must be 0."; - assert assertMsg (nHosts <= capacity - nInit) - "assignMacs: number of hosts (${toString nHosts}) must be <= capacity (${toString capacity}) - reserved (${toString nInit})"; - # Assign an ip in the subnet to each element, in order - (foldl' assignOne { - assigned = {}; - used = init; - } - sortedHosts) - .assigned; - }; - }; - }; -} diff --git a/modules/default.nix b/modules/default.nix index 4ed4918..71d56e0 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -7,7 +7,6 @@ ./config/impermanence.nix ./config/inputrc.nix ./config/issue.nix - ./config/lib.nix ./config/microvms.nix ./config/net.nix ./config/nftables.nix diff --git a/modules/meta/microvms.nix b/modules/meta/microvms.nix index af30a0b..7c4e071 100644 --- a/modules/meta/microvms.nix +++ b/modules/meta/microvms.nix @@ -30,18 +30,25 @@ types ; + inherit + (import ../../lib/misc.nix inputs) + mergeToplevelConfigs + ; + + net = import ../../lib/net.nix inputs; + disko = import ../../lib/disko.nix inputs; + parentConfig = config; cfg = config.meta.microvms; - nodeName = config.repo.node.name; + nodeName = config.node.name; inherit (cfg) vms; - inherit (config.lib) net; # Configuration for each microvm microvmConfig = vmName: vmCfg: { # Add the required datasets to the disko configuration of the machine disko.devices.zpool = mkIf vmCfg.zfs.enable { ${vmCfg.zfs.pool}.datasets."${vmCfg.zfs.dataset}" = - config.lib.disko.zfs.filesystem vmCfg.zfs.mountpoint; + disko.zfs.filesystem vmCfg.zfs.mountpoint; }; # Ensure that the zfs dataset exists before it is mounted. @@ -107,7 +114,7 @@ inherit (node) pkgs; inherit (vmCfg) autostart; config = {config, ...}: { - imports = cfg.commonImports ++ node.imports; + imports = cfg.commonImports ++ node.imports ++ vmCfg.modules; microvm = { hypervisor = mkDefault "cloud-hypervisor"; @@ -332,6 +339,12 @@ in { type = types.str; description = mdDoc "The system that this microvm should use"; }; + + modules = mkOption { + type = types.listOf types.unspecified; + default = []; + description = mdDoc "Additional modules to load"; + }; }; })); }; @@ -356,6 +369,6 @@ in { }; }; } - // config.lib.misc.mergeToplevelConfigs ["nodes" "disko" "microvm" "systemd"] (mapAttrsToList microvmConfig vms) + // mergeToplevelConfigs ["nodes" "disko" "microvm" "systemd"] (mapAttrsToList microvmConfig vms) ); } diff --git a/modules/meta/promtail.nix b/modules/meta/promtail.nix index 9c43b08..a28e914 100644 --- a/modules/meta/promtail.nix +++ b/modules/meta/promtail.nix @@ -46,7 +46,7 @@ in { clients = [ { - basic_auth.username = "${config.repo.node.name}+promtail-loki-basic-auth-password"; + basic_auth.username = "${config.node.name}+promtail-loki-basic-auth-password"; basic_auth.password_file = config.age.secrets.promtail-loki-basic-auth-password.path; url = "https://${nodes.${cfg.proxy}.config.networking.providedDomains.loki}/loki/api/v1/push"; } diff --git a/modules/meta/telegraf.nix b/modules/meta/telegraf.nix index 3374900..817cdb1 100644 --- a/modules/meta/telegraf.nix +++ b/modules/meta/telegraf.nix @@ -16,7 +16,7 @@ ; cfg = config.meta.telegraf; - nodeName = config.repo.node.name; + nodeName = config.node.name; in { options.meta.telegraf = { enable = mkEnableOption (mdDoc "telegraf to push metrics to influx."); diff --git a/modules/meta/wireguard.nix b/modules/meta/wireguard.nix index 61e5e01..190ea53 100644 --- a/modules/meta/wireguard.nix +++ b/modules/meta/wireguard.nix @@ -41,181 +41,27 @@ ; inherit - (config.lib.misc) + (import ../../lib/misc.nix inputs) concatAttrs duplicates mergeToplevelConfigs ; inherit - (config.lib.types) + (import ../../lib/types.nix inputs) lazyOf lazyValue ; - inherit (config.lib) net; + net = import ../../lib/net.nix inputs; + wgLibFor = import ../../lib/wireguard.nix inputs; + cfg = config.meta.wireguard; - nodeName = config.repo.node.name; - - libFor = wgName: rec { - # Returns the given node's wireguard configuration of this network - wgCfgOf = node: nodes.${node}.config.meta.wireguard.${wgName}; - - sortedPeers = peerA: peerB: - if peerA < peerB - then { - peer1 = peerA; - peer2 = peerB; - } - else { - peer1 = peerB; - peer2 = peerA; - }; - - peerPublicKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.pub"; - peerPublicKeyPath = peerName: inputs.self.outPath + peerPublicKeyFile peerName; - - peerPrivateKeyFile = peerName: "/secrets/wireguard/${wgName}/keys/${peerName}.age"; - peerPrivateKeyPath = peerName: inputs.self.outPath + peerPrivateKeyFile peerName; - peerPrivateKeySecret = peerName: "wireguard-${wgName}-priv-${peerName}"; - - peerPresharedKeyFile = peerA: peerB: let - inherit (sortedPeers peerA peerB) peer1 peer2; - in "/secrets/wireguard/${wgName}/psks/${peer1}+${peer2}.age"; - peerPresharedKeyPath = peerA: peerB: inputs.self.outPath + peerPresharedKeyFile peerA peerB; - peerPresharedKeySecret = peerA: peerB: let - inherit (sortedPeers peerA peerB) peer1 peer2; - in "wireguard-${wgName}-psks-${peer1}+${peer2}"; - - # All nodes that are part of this network - participatingNodes = - filter - (n: builtins.hasAttr wgName nodes.${n}.config.meta.wireguard) - (attrNames nodes); - - # Partition nodes by whether they are servers - _participatingNodes_isServerPartition = - partition - (n: (wgCfgOf n).server.host != null) - participatingNodes; - - participatingServerNodes = _participatingNodes_isServerPartition.right; - participatingClientNodes = _participatingNodes_isServerPartition.wrong; - - # Maps all nodes that are part of this network to their addresses - nodePeers = genAttrs participatingNodes (n: (wgCfgOf n).addresses); - - externalPeerName = p: "external-${p}"; - - # Only peers that are defined as externalPeers on the given node. - # Prepends "external-" to their name. - externalPeersForNode = node: - mapAttrs' (p: nameValuePair (externalPeerName p)) (wgCfgOf node).server.externalPeers; - - # All peers that are defined as externalPeers on any node. - # Prepends "external-" to their name. - 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) participatingNodes; - - # A list of all occurring addresses. - usedAddresses = - concatMap (n: (wgCfgOf n).addresses) participatingNodes - ++ flatten (concatMap (n: attrValues (wgCfgOf n).server.externalPeers) participatingNodes); - - # 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 - (nodes.${n}.options.meta.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) 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 - 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 - # storing them in the nix-store. - wgQuickConfigScript = system: serverNode: extPeer: let - pkgs = inputs.self.pkgs.${system}; - snCfg = wgCfgOf serverNode; - 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 ${config.lib.secrets.rageDecryptArgs} ${escapeShellArg (peerPrivateKeyPath peerName)}) \ - || { echo "error: Failed to decrypt!" >&2; exit 1; } - serverPsk=$(${pkgs.rage}/bin/rage -d ${config.lib.secrets.rageDecryptArgs} ${escapeShellArg (peerPresharedKeyPath serverNode peerName)}) \ - || { echo "error: Failed to decrypt!" >&2; exit 1; } - - cat <