diff --git a/README.md b/README.md index 21c9eb5..7e36ee6 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ This is my personal nix config. It's still in the making, but this is what I got so far: -- Full disk encryption using [disko](https://github.com/nix-community/disko), remotely unlockable via ssh -- Zoned nftables firewall -- Service isolation using [microvms](https://github.com/astro/microvm.nix) instead of containers - Log and system monitoring via loki, telegraf, influxdb, promtail and grafana - Single-Sign-On for all services using oauth2 via kanidm - Automatic wireguard mesh generation +- Full disk encryption using [disko](https://github.com/nix-community/disko), remotely unlockable via ssh +- Zoned nftables firewall via [nixos-nftables-firewall](https://github.com/thelegy/nixos-nftables-firewall) +- Service isolation using [microvms](https://github.com/astro/microvm.nix) instead of containers - Secret rekeying, generation and bootstrapping using [agenix-rekey](https://github.com/oddlama/agenix-rekey) - Support for repository-wide secrets at evaluation time (hides PII like MACs) diff --git a/flake.nix b/flake.nix index 60a5bb5..2efd995 100644 --- a/flake.nix +++ b/flake.nix @@ -131,7 +131,10 @@ pkgs = import nixpkgs { localSystem = system; config.allowUnfree = true; - overlays = [microvm.overlay] ++ import ./pkgs/default.nix; + overlays = + import ./lib inputs + ++ import ./pkgs/default.nix + ++ [microvm.overlay]; }; apps = diff --git a/hosts/nom/fs.nix b/hosts/nom/fs.nix index eb72029..907f291 100644 --- a/hosts/nom/fs.nix +++ b/hosts/nom/fs.nix @@ -1,16 +1,14 @@ { - inputs, config, + lib, ... -}: 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 disko.gpt; { + content = with lib.disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -21,7 +19,7 @@ in { boot-ssd = { type = "disk"; device = "/dev/disk/by-id/${config.repo.secrets.local.disk.boot-ssd}"; - content = with disko.gpt; { + content = with lib.disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -31,7 +29,7 @@ in { }; }; }; - zpool = with disko.zfs; { + zpool = with lib.disko.zfs; { rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;}; }; }; diff --git a/hosts/sentinel/fs.nix b/hosts/sentinel/fs.nix index 1e2122a..af216db 100644 --- a/hosts/sentinel/fs.nix +++ b/hosts/sentinel/fs.nix @@ -1,16 +1,14 @@ { config, - inputs, + lib, ... -}: 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 disko.gpt; { + content = with lib.disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -21,7 +19,7 @@ in { }; }; }; - zpool = with disko.zfs; { + zpool = with lib.disko.zfs; { rpool = defaultZpoolOptions // {datasets = defaultZfsDatasets;}; }; }; diff --git a/hosts/ward/fs.nix b/hosts/ward/fs.nix index 1d31b9f..e24ed85 100644 --- a/hosts/ward/fs.nix +++ b/hosts/ward/fs.nix @@ -1,18 +1,15 @@ { 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 disko.gpt; { + content = with lib.disko.gpt; { type = "table"; format = "gpt"; partitions = [ @@ -23,7 +20,7 @@ in { }; }; }; - zpool = with disko.zfs; { + zpool = with lib.disko.zfs; { rpool = defaultZpoolOptions // { diff --git a/hosts/ward/net.nix b/hosts/ward/net.nix index e8667ee..a4d4cf5 100644 --- a/hosts/ward/net.nix +++ b/hosts/ward/net.nix @@ -1,15 +1,9 @@ { config, - inputs, lib, utils, ... }: let - inherit - (import ../../lib/net.nix inputs) - cidr - ; - lanCidrv4 = "192.168.100.0/24"; lanCidrv6 = "fd10::/64"; in { @@ -60,8 +54,8 @@ in { }; "20-lan-self" = { address = [ - (cidr.hostCidr 1 lanCidrv4) - (cidr.hostCidr 1 lanCidrv6) + (lib.net.cidr.hostCidr 1 lanCidrv4) + (lib.net.cidr.hostCidr 1 lanCidrv6) ]; matchConfig.Name = "lan-self"; networkConfig = { @@ -84,7 +78,7 @@ in { ipv6SendRAConfig = { EmitDNS = true; # TODO change to self later - #DNS = cidr.host 1 net.lan.ipv6cidr; + #DNS = lib.net.cidr.host 1 net.lan.ipv6cidr; DNS = ["2606:4700:4700::1111" "2001:4860:4860::8888"]; }; linkConfig.RequiredForOnline = "routable"; @@ -160,12 +154,12 @@ in { interface = "lan-self"; subnet = lanCidrv4; pools = [ - {pool = "${cidr.host 20 lanCidrv4} - ${cidr.host (-6) lanCidrv4}";} + {pool = "${lib.net.cidr.host 20 lanCidrv4} - ${lib.net.cidr.host (-6) lanCidrv4}";} ]; option-data = [ { name = "routers"; - data = cidr.host 1 lanCidrv4; + data = lib.net.cidr.host 1 lanCidrv4; } ]; } diff --git a/hosts/zackbiene/net.nix b/hosts/zackbiene/net.nix index 3831e9e..77e8e4d 100644 --- a/hosts/zackbiene/net.nix +++ b/hosts/zackbiene/net.nix @@ -1,14 +1,8 @@ { config, - inputs, lib, ... }: let - inherit - (import ../../lib/net.nix inputs) - cidr - ; - iotCidrv4 = "10.90.0.0/24"; iotCidrv6 = "fd00:90::/64"; in { @@ -31,8 +25,8 @@ in { }; "10-wlan1" = { address = [ - (cidr.hostCidr 1 iotCidrv4) - (cidr.hostCidr 1 iotCidrv6) + (lib.net.cidr.hostCidr 1 iotCidrv4) + (lib.net.cidr.hostCidr 1 iotCidrv6) ]; matchConfig.MACAddress = config.repo.secrets.local.networking.interfaces.wlan1.mac; linkConfig.RequiredForOnline = "no"; diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..20309b9 --- /dev/null +++ b/lib/default.nix @@ -0,0 +1,7 @@ +inputs: [ + (import ./disko.nix inputs) + (import ./misc.nix inputs) + (import ./net.nix inputs) + (import ./types.nix inputs) + (import ./wireguard.nix inputs) +] diff --git a/lib/disko.nix b/lib/disko.nix index a6d8b80..6e0e64c 100644 --- a/lib/disko.nix +++ b/lib/disko.nix @@ -1,81 +1,87 @@ -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; +inputs: self: super: { + lib = + super.lib + // { + 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; + }; }; }; }; - }; - 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 index 3a60cc4..38ba180 100644 --- a/lib/misc.nix +++ b/lib/misc.nix @@ -1,54 +1,25 @@ -inputs: let +inputs: self: super: let inherit - (inputs.nixpkgs.lib) - all - any - assertMsg - attrNames - attrValues - concatLists - concatMap + (super.lib) 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 {}; + + # Counts how often each element occurrs in xs. + # Elements must be strings. + countOccurrences = + foldl' + (acc: x: acc // {${x} = (acc.${x} or 0) + 1;}) + {}; # Returns all elements in xs that occur at least twice duplicates = xs: let @@ -70,50 +41,67 @@ in rec { # Calculates base^exp, but careful, this overflows for results > 2^62 pow = base: exp: foldl' (a: x: x * a) 1 (genList (_: base) exp); + hexLiteralValues = { + "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; + }; + # 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); + hexToDec = v: foldl' (acc: x: acc * 16 + hexLiteralValues.${x}) 0 (stringToCharacters v); +in { + lib = + super.lib + // { + inherit + concatAttrs + countOccurrences + duplicates + hexToDec + isAbsolutePath + mergeToplevelConfigs + pow + ; - 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}"; - }; + # TODO separate this or get rid of it + # TODO separate this or get rid of it + # TODO separate this or get rid of it + # TODO separate this or get rid of it + 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 index 849ed70..5dba681 100644 --- a/lib/net.nix +++ b/lib/net.nix @@ -1,50 +1,28 @@ -inputs: let +inputs: self: super: 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) + (self.lib) hexToDec pow ; @@ -58,321 +36,325 @@ inputs: let }) .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; +in { + lib = recursiveUpdate super.lib { + net = recursiveUpdate (removeAttrs libNet ["types"]) { + 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 (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; }; - # 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; - }; - } + types.net = libNet.types; + }; +} diff --git a/lib/types.nix b/lib/types.nix index cc19775..bcfc18f 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -1,48 +1,15 @@ -inputs: let +inputs: self: super: 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; @@ -71,4 +38,15 @@ in rec { # 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; +in { + lib = recursiveUpdate super.lib { + types = { + inherit + isLazyValue + lazyValue + lazyValueOf + lazyOf + ; + }; + }; } diff --git a/lib/wireguard.nix b/lib/wireguard.nix index 18d6a8d..2ea11e7 100644 --- a/lib/wireguard.nix +++ b/lib/wireguard.nix @@ -1,218 +1,225 @@ -inputs: wgName: let +inputs: self: super: 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) + (self.lib) + net concatAttrs + types ; inherit - (misc.secrets) + (self.lib.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}; +in { + lib = + super.lib + // { + wireguard = wgName: let + # 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; + 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 ${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 <&2; exit 1; } - serverPsk=$(${pkgs.rage}/bin/rage -d ${rageDecryptArgs} ${escapeShellArg (peerPresharedKeyPath serverNode peerName)}) \ - || { echo "error: Failed to decrypt!" >&2; exit 1; } - - cat <