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

refactor: finish decoupling the library functions from config

This commit is contained in:
oddlama 2023-07-01 01:11:58 +02:00
parent 68bb9731d3
commit 80e7c1bdbf
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
59 changed files with 984 additions and 786 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

81
lib/disko.nix Normal file
View file

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

119
lib/misc.nix Normal file
View file

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

378
lib/net.nix Normal file
View file

@ -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"
# <fails with an error message>
# > net.cidr.host (-1) "192.168.1.0/24"
# "192.168.1.255"
# > net.cidr.host (-256) "192.168.1.0/24"
# "192.168.1.0"
# > net.cidr.host (-257) "192.168.1.0/24"
# <fails with an error message>
host = i: n: let
cap = 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;
};
}

74
lib/types.nix Normal file
View file

@ -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 = <actual value>; }.
# This allows the option to be defined and filtered from a defintion
# list without evaluating the value.
lazyValueOf = type:
mkOptionType rec {
name = "lazyValueOf ${type.name}";
inherit (type) description descriptionClass emptyValue getSubOptions getSubModules;
check = isLazyValue;
merge = loc: defs:
assert assertMsg
(all (x: type.check x._lazyValue) defs)
"The option `${showOption loc}` is defined with a lazy value holding an invalid type";
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;
}

218
lib/wireguard.nix Normal file
View file

@ -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 <<EOF
[Interface]
Address = ${concatStringsSep ", " addresses}
PrivateKey = $privKey
[Peer]
PublicKey = ${removeSuffix "\n" (builtins.readFile (peerPublicKeyPath serverNode))}
PresharedKey = $serverPsk
AllowedIPs = ${concatStringsSep ", " networkCidrs}
Endpoint = ${snCfg.server.host}:${toString snCfg.server.port}
PersistentKeepalive = 25
EOF
'';
}

View file

@ -1,560 +0,0 @@
{
inputs,
lib,
...
}: let
inherit
(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 {
# IP address math library
# https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba
# Plus some extensions by us
lib = let
libWithNet = (import "${inputs.lib-net}/net.nix" {inherit lib;}).lib;
in
recursiveUpdate libWithNet {
types = rec {
# Checks whether the value is a lazy value without causing
# it's value to be evaluated
isLazyValue = x: isAttrs x && x ? _lazyValue;
# Constructs a lazy value holding the given value.
lazyValue = value: {_lazyValue = value;};
# Represents a lazy value of the given type, which
# holds the actual value as an attrset like { _lazyValue = <actual value>; }.
# This allows the option to be defined and filtered from a defintion
# list without evaluating the value.
lazyValueOf = type:
mkOptionType rec {
name = "lazyValueOf ${type.name}";
inherit (type) description descriptionClass emptyValue getSubOptions getSubModules;
check = isLazyValue;
merge = loc: defs:
assert assertMsg
(all (x: type.check x._lazyValue) defs)
"The option `${showOption loc}` is defined with a lazy value holding an invalid type";
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"
# <fails with an error message>
# > net.cidr.host (-1) "192.168.1.0/24"
# "192.168.1.255"
# > net.cidr.host (-256) "192.168.1.0/24"
# "192.168.1.0"
# > net.cidr.host (-257) "192.168.1.0/24"
# <fails with an error message>
host = i: n: let
cap = libWithNet.net.cidr.capacity n;
in
assert 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;
};
};
};
}

View file

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

View file

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

View file

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

View file

@ -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.");

View file

@ -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 <<EOF
[Interface]
Address = ${concatStringsSep ", " addresses}
PrivateKey = $privKey
[Peer]
PublicKey = ${removeSuffix "\n" (builtins.readFile (peerPublicKeyPath serverNode))}
PresharedKey = $serverPsk
AllowedIPs = ${concatStringsSep ", " networkCidrs}
Endpoint = ${snCfg.server.host}:${toString snCfg.server.port}
PersistentKeepalive = 25
EOF
'';
};
nodeName = config.node.name;
configForNetwork = wgName: wgCfg: let
inherit
(libFor wgName)
(wgLibFor wgName)
externalPeerName
externalPeerNamesRaw
networkCidrs
@ -532,7 +378,7 @@ in {
ipv4 = mkOption {
type = lazyOf net.types.ipv4;
default = lazyValue (libFor name).assignedIpv4Addresses.${nodeName};
default = lazyValue (wgLibFor name).assignedIpv4Addresses.${nodeName};
description = mdDoc ''
The ipv4 address for this machine. If you do not set this explicitly,
a semi-stable ipv4 address will be derived automatically based on the
@ -544,7 +390,7 @@ in {
ipv6 = mkOption {
type = lazyOf net.types.ipv6;
default = lazyValue (libFor name).assignedIpv6Addresses.${nodeName};
default = lazyValue (wgLibFor name).assignedIpv6Addresses.${nodeName};
description = mdDoc ''
The ipv6 address for this machine. If you do not set this explicitly,
a semi-stable ipv6 address will be derived automatically based on the

View file

@ -1,6 +1,7 @@
# Provides an option to easily rename interfaces by their mac addresses.
{
config,
inputs,
lib,
pkgs,
...
@ -15,6 +16,11 @@
types
;
inherit
(import ../../lib/misc.nix inputs)
duplicates
;
cfg = config.networking.renameInterfacesByMac;
interfaceNamesUdevRules = pkgs.writeTextFile {
@ -34,7 +40,7 @@ in {
config = lib.mkIf (cfg != {}) {
assertions = let
duplicateMacs = config.lib.misc.duplicates (attrValues cfg);
duplicateMacs = duplicates (attrValues cfg);
in [
{
assertion = duplicateMacs == [];

View file

@ -2,6 +2,7 @@
config,
inputs,
lib,
options,
...
}: let
inherit
@ -9,34 +10,49 @@
attrNames
concatMap
elem
foldl'
getAttrFromPath
mdDoc
mkIf
mkOption
mkOptionType
mkMerge
optionalAttrs
recursiveUpdate
hasAttrByPath
setAttrByPath
types
;
nodeName = config.repo.node.name;
nodeName = config.node.name;
in {
options.nodes = mkOption {
default = {};
description = mdDoc "Allows extending the configuration of other machines.";
type = types.attrsOf (mkOptionType {
name = "Toplevel NixOS config";
merge = loc: map (x: x.value);
});
default = {};
description = mdDoc "Allows extending the configuration of other machines.";
};
config = let
allNodes = attrNames inputs.self.colmenaNodes;
isColmenaNode = elem nodeName allNodes;
foreignConfigs = concatMap (n: inputs.self.colmenaNodes.${n}.config.nodes.${nodeName} or []) allNodes;
toplevelAttrs = ["age" "networking" "systemd" "services"];
relevantConfigs = foreignConfigs ++ [config.nodes.${nodeName} or {}];
mergeFromOthers = path:
mkMerge (map
(x: mkIf (hasAttrByPath path x) (getAttrFromPath path x))
relevantConfigs);
pathsToMerge = [
["age" "secrets"]
["networking" "providedDomains"]
["services" "nginx" "upstreams"]
["services" "nginx" "virtualHosts"]
];
in
optionalAttrs isColmenaNode (config.lib.misc.mergeToplevelConfigs toplevelAttrs (
foreignConfigs
# Also allow extending ourselves, in case some attributes from depenent
# configurations such as containers or microvms are merged to the host
++ [config.nodes.${nodeName} or {}]
));
mkIf isColmenaNode (foldl'
(acc: path: recursiveUpdate acc (setAttrByPath path (mergeFromOthers path)))
{}
pathsToMerge);
}

View file

@ -15,7 +15,10 @@
import ./generate-node.nix inputs {
inherit name;
inherit (hostCfg) system;
modules = [../hosts/${name}];
modules = [
../hosts/${name}
{node.secretsDir = ../hosts/${name}/secrets;}
];
});
in
{

View file

@ -39,7 +39,7 @@
...
}: {
isoImage.isoName = lib.mkForce "nixos-image-${nodeName}.iso";
system.stateVersion = self.stateVersion;
system.stateVersion = nodeAttrs.system.stateVersion;
nix.extraOptions = ''
experimental-features = nix-command flakes
'';

View file

@ -29,7 +29,7 @@
imports =
modules
++ [
{repo.node.name = name;}
{node.name = name;}
agenix.nixosModules.default
agenix-rekey.nixosModules.default
disko.nixosModules.disko

View file

@ -2,7 +2,6 @@
config,
lib,
pkgs,
stateVersion,
...
}: let
myuser = config.repo.secrets.global.myuser.name;
@ -31,7 +30,7 @@ in {
];
home = {
inherit stateVersion;
inherit (config.system) stateVersion;
inherit (config.users.users.${myuser}) uid;
username = config.users.users.${myuser}.name;
shellAliases = {

View file

@ -2,7 +2,6 @@
config,
lib,
pkgs,
stateVersion,
...
}: {
users.users.root = {
@ -17,7 +16,7 @@
];
home = {
inherit stateVersion;
inherit (config.system) stateVersion;
inherit (config.users.users.root) uid;
username = config.users.users.root.name;