diff --git a/flake-parts/machines/atlas.nix b/flake-parts/machines/atlas.nix new file mode 100644 index 0000000..a6bf86f --- /dev/null +++ b/flake-parts/machines/atlas.nix @@ -0,0 +1,16 @@ +{ + machines.atlas = { + arch = "x86_64-linux"; + kubernetesNodeLabels.storageType = "slow"; + + nixosModule.lab = { + storage.profile = "kubernetes"; + tailscale.enable = true; + + k3s = { + enable = true; + serverAddr = "https://jefke.dmz:6443"; + }; + }; + }; +} diff --git a/flake-parts/machines/default.nix b/flake-parts/machines/default.nix new file mode 100644 index 0000000..74aa74e --- /dev/null +++ b/flake-parts/machines/default.nix @@ -0,0 +1,59 @@ +{ nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: +let + pkgs = nixpkgs.legacyPackages.${system}; + lib = pkgs.lib; + + machineOpts = { config, ... }: { + options = { + arch = lib.mkOption { + default = null; + type = with lib.types; nullOr str; + description = '' + CPU architecture of this machine. + ''; + }; + + isRaspberryPi = lib.mkOption { + default = false; + type = lib.types.bool; + }; + + nixosModule = lib.mkOption { + default = { ... }: { }; + type = lib.types.anything; + description = '' + Customized configuration for this machine in the form of a NixOS module. + ''; + }; + + kubernetesNodeLabels = lib.mkOption { + default = null; + type = with lib.types; nullOr attrs; + description = '' + Any labels to add to the Kubernetes node. + ''; + }; + }; + }; + + allOpts = { + options = { + machines = lib.mkOption { + type = with lib.types; attrsOf (submodule machineOpts); + }; + }; + }; +in +{ + machines = (lib.modules.evalModules { + modules = [ + allOpts + ./warwick.nix + ./atlas.nix + ./jefke.nix + ./lewis.nix + # ./talos.nix + # ./pikvm.nix + ]; + }).config.machines; +}) diff --git a/flake-parts/machines/jefke.nix b/flake-parts/machines/jefke.nix new file mode 100644 index 0000000..096be8c --- /dev/null +++ b/flake-parts/machines/jefke.nix @@ -0,0 +1,16 @@ +{ + machines.jefke = { + arch = "x86_64-linux"; + kubernetesNodeLabels.storageType = "fast"; + + nixosModule.lab = { + storage.profile = "kubernetes"; + tailscale.enable = true; + + k3s = { + enable = true; + clusterInit = true; + }; + }; + }; +} diff --git a/flake-parts/machines/lewis.nix b/flake-parts/machines/lewis.nix new file mode 100644 index 0000000..5350142 --- /dev/null +++ b/flake-parts/machines/lewis.nix @@ -0,0 +1,23 @@ +{ + machines.lewis = { + arch = "x86_64-linux"; + kubernetesNodeLabels = { + storageType = "fast"; + hasMedia = "true"; + }; + + nixosModule = { + lab = { + storage.profile = "kubernetes"; + backups.enable = true; + data-sharing.enable = true; + tailscale.enable = true; + + k3s = { + enable = true; + serverAddr = "https://jefke.dmz:6443"; + }; + }; + }; + }; +} diff --git a/flake-parts/machines/pikvm.nix b/flake-parts/machines/pikvm.nix new file mode 100644 index 0000000..6a7bc14 --- /dev/null +++ b/flake-parts/machines/pikvm.nix @@ -0,0 +1,23 @@ +{ + machines.pikvm = { + arch = "aarch64-linux"; + isRaspberryPi = true; + + nixosModule = { config, inputs, lib, ... }: { + # imports = [ "${inputs.nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix" ]; + lab = { + storage.profile = "pi"; + }; + + environment.systemPackages = with inputs.nixpkgs.legacyPackages.aarch64-linux; [ + (mplayer.override { + v4lSupport = true; + }) + ffmpeg + v4l-utils + ]; + + boot.extraModulePackages = with config.boot.kernelPackages; [ v4l2loopback ]; + }; + }; +} diff --git a/flake-parts/machines/talos.nix b/flake-parts/machines/talos.nix new file mode 100644 index 0000000..0fa0311 --- /dev/null +++ b/flake-parts/machines/talos.nix @@ -0,0 +1,11 @@ +{ + machines.talos = { + arch = "x86_64-linux"; + + nixosModule = { lib, ... }: { + lab.storage.profile = "normal"; + + # boot.loader.systemd-boot.enable = lib.mkForce false; + }; + }; +} diff --git a/flake-parts/machines/warwick.nix b/flake-parts/machines/warwick.nix new file mode 100644 index 0000000..f000881 --- /dev/null +++ b/flake-parts/machines/warwick.nix @@ -0,0 +1,18 @@ +{ + machines.warwick = { + arch = "aarch64-linux"; + isRaspberryPi = true; + + nixosModule = { lib, ... }: { + lab = { + storage.profile = "pi"; + monitoring.server.enable = true; + + tailscale = { + advertiseExitNode = true; + enable = true; + }; + }; + }; + }; +} diff --git a/flake-parts/utils/default.nix b/flake-parts/utils/default.nix new file mode 100644 index 0000000..e0ecf4e --- /dev/null +++ b/flake-parts/utils/default.nix @@ -0,0 +1,20 @@ +{ nixpkgs, flake-utils, ... }: + +let + systemAttrs = flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacypackages.${system}; + lib = pkgs.lib; + in + { + net = import ./net.nix lib; + }); + + nonSystemAttrs = rec { + globals = import ./globals.nix; + imagePath = name: "nix:0${globals.imageDir}/${name}.tar"; + }; + + allAttrs = systemAttrs // nonSystemAttrs; +in +allAttrs diff --git a/flake-parts/utils/globals.nix b/flake-parts/utils/globals.nix new file mode 100644 index 0000000..38aadd5 --- /dev/null +++ b/flake-parts/utils/globals.nix @@ -0,0 +1,65 @@ +{ + routerPublicIPv4 = "192.145.57.90"; + routerPublicIPv6 = "2a0d:6e00:1a77::1"; + bind9Ipv6 = "2a0d:6e00:1a77:30::134"; + + # Load balancer IPv4 + traefikIPv4 = "192.168.30.128"; + kmsIPv4 = "192.168.30.129"; + inbucketIPv4 = "192.168.30.130"; + piholeIPv4 = "192.168.30.131"; + gitIPv4 = "192.168.30.132"; + transmissionIPv4 = "192.168.30.133"; + bind9IPv4 = "192.168.30.134"; + dnsmasqIPv4 = "192.168.30.135"; + minecraftIPv4 = "192.168.30.136"; + jellyseerrIPv4 = "192.168.30.137"; + syncthingIPv4 = "192.168.30.138"; + longhornIPv4 = "192.168.30.139"; + radarrIPv4 = "192.168.30.140"; + prowlarrIPv4 = "192.168.30.141"; + sonarrIPv4 = "192.168.30.142"; + bazarrIPv4 = "192.168.30.143"; + paperlessIPv4 = "192.168.30.144"; + radicaleIPv4 = "192.168.30.145"; + freshrssIPv4 = "192.168.30.146"; + immichIPv4 = "192.168.30.147"; + nextcloudIPv4 = "192.168.30.148"; + + imageDir = "/var/docker_images"; + + images = { + jellyfin = "jellyfin/jellyfin:10.9.9"; + deluge = "linuxserver/deluge:2.1.1"; + jellyseerr = "fallenbagel/jellyseerr:1.9.2"; + radarr = "lscr.io/linuxserver/radarr:5.9.1"; + prowlarr = "lscr.io/linuxserver/prowlarr:1.21.2"; + sonarr = "lscr.io/linuxserver/sonarr:4.0.8"; + bazarr = "lscr.io/linuxserver/bazarr:1.4.3"; + atuin = "ghcr.io/atuinsh/atuin:18.3.0"; + postgres14 = "postgres:14"; + kms = "teddysun/kms:latest"; + paperless = "ghcr.io/paperless-ngx/paperless-ngx:2.11.6"; + redis7 = "docker.io/library/redis:7"; + nextcloud = "nextcloud:29.0.5"; + postgres15 = "postgres:15"; + inbucket = "inbucket/inbucket:edge"; + syncthing = "lscr.io/linuxserver/syncthing:1.27.10"; + radicale = "tomsquest/docker-radicale:3.2.3.0"; + ntfy = "binwiederhier/ntfy:v2.11.0"; + forgejo = "codeberg.org/forgejo/forgejo:8.0.1"; + pihole = "pihole/pihole:2024.07.0"; + immich = "ghcr.io/immich-app/immich-server:v1.114.0"; + immich-machine-learning = "ghcr.io/immich-app/immich-machine-learning:v1.114.0"; + immich-redis = "docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e"; + immich-postgres = "docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0"; + kitchenowl = "tombursch/kitchenowl:v0.5.2"; + cyberchef = "mpepping/cyberchef:latest"; + freshrss = "freshrss/freshrss:1.24.3"; + bind9 = "ubuntu/bind9:9.18-22.04_beta"; + dnsmasq = "dockurr/dnsmasq:2.90"; + attic = "git.kun.is/home/atticd:fd910d91c2143295e959d2c903e9ea25cf94ba27"; + hedgedoc = "quay.io/hedgedoc/hedgedoc:1.9.9"; + minecraft = "itzg/minecraft-server:latest"; + }; +} diff --git a/flake-parts/utils/net.nix b/flake-parts/utils/net.nix new file mode 100644 index 0000000..9f5b0e5 --- /dev/null +++ b/flake-parts/utils/net.nix @@ -0,0 +1,1323 @@ +# IP address arithmetic and validation in Nix by @duairc: +# https://gist.github.com/duairc/5c9bb3c922e5d501a1edb9e7b3b845ba + +lib: + +let + + net = { + ip = { + + # add :: (ip | mac | integer) -> ip -> ip + # + # Examples: + # + # Adding integer to IPv4: + # > net.ip.add 100 "10.0.0.1" + # "10.0.0.101" + # + # Adding IPv4 to IPv4: + # > net.ip.add "127.0.0.1" "10.0.0.1" + # "137.0.0.2" + # + # Adding IPv6 to IPv4: + # > net.ip.add "::cafe:beef" "10.0.0.1" + # "212.254.186.191" + # + # Adding MAC to IPv4 (overflows): + # > net.ip.add "fe:ed:fa:ce:f0:0d" "10.0.0.1" + # "4.206.240.14" + # + # Adding integer to IPv6: + # > net.ip.add 100 "dead:cafe:beef::" + # "dead:cafe:beef::64" + # + # Adding IPv4 to to IPv6: + # > net.ip.add "127.0.0.1" "dead:cafe:beef::" + # "dead:cafe:beef::7f00:1" + # + # Adding MAC to IPv6: + # > net.ip.add "fe:ed:fa:ce:f0:0d" "dead:cafe:beef::" + # "dead:cafe:beef::feed:face:f00d" + add = delta: ip: + let + function = "net.ip.add"; + delta' = typechecks.numeric function "delta" delta; + ip' = typechecks.ip function "ip" ip; + in + builders.ip (implementations.ip.add delta' ip'); + + # diff :: ip -> ip -> (integer | ipv6) + # + # net.ip.diff is the reverse of net.ip.add: + # + # net.ip.diff (net.ip.add a b) a = b + # net.ip.diff (net.ip.add a b) b = a + # + # The difference between net.ip.diff and net.ip.subtract is that + # net.ip.diff will try its best to return an integer (falling back + # to an IPv6 if the result is too big to fit in an integer). This is + # useful if you have two hosts that you know are on the same network + # and you just want to calculate the offset between them — a result + # like "0.0.0.10" is not very useful (which is what you would get + # from net.ip.subtract). + diff = minuend: subtrahend: + let + function = "net.ip.diff"; + minuend' = typechecks.ip function "minuend" minuend; + subtrahend' = typechecks.ip function "subtrahend" subtrahend; + result = implementations.ip.diff minuend' subtrahend'; + in + if result ? ipv6 + then builders.ipv6 result + else result; + + # subtract :: (ip | mac | integer) -> ip -> ip + # + # net.ip.subtract is also the reverse of net.ip.add: + # + # net.ip.subtract a (net.ip.add a b) = b + # net.ip.subtract b (net.ip.add a b) = a + # + # The difference between net.ip.subtract and net.ip.diff is that + # net.ip.subtract will always return the same type as its "ip" + # parameter. Its implementation takes the "delta" parameter, + # coerces it to be the same type as the "ip" paramter, negates it + # (using two's complement), and then adds it to "ip". + subtract = delta: ip: + let + function = "net.ip.subtract"; + delta' = typechecks.numeric function "delta" delta; + ip' = typechecks.ip function "ip" ip; + in + builders.ip (implementations.ip.subtract delta' ip'); + }; + + mac = { + + # add :: (ip | mac | integer) -> mac -> mac + # + # Examples: + # + # Adding integer to MAC: + # > net.mac.add 100 "fe:ed:fa:ce:f0:0d" + # "fe:ed:fa:ce:f0:71" + # + # Adding IPv4 to MAC: + # > net.mac.add "127.0.0.1" "fe:ed:fa:ce:f0:0d" + # "fe:ee:79:ce:f0:0e" + # + # Adding IPv6 to MAC: + # > net.mac.add "::cafe:beef" "fe:ed:fa:ce:f0:0d" + # "fe:ee:c5:cd:aa:cb + # + # Adding MAC to MAC: + # > net.mac.add "fe:ed:fa:00:00:00" "00:00:00:ce:f0:0d" + # "fe:ed:fa:ce:f0:0d" + add = delta: mac: + let + function = "net.mac.add"; + delta' = typechecks.numeric function "delta" delta; + mac' = typechecks.mac function "mac" mac; + in + builders.mac (implementations.mac.add delta' mac'); + + # diff :: mac -> mac -> integer + # + # net.mac.diff is the reverse of net.mac.add: + # + # net.mac.diff (net.mac.add a b) a = b + # net.mac.diff (net.mac.add a b) b = a + # + # The difference between net.mac.diff and net.mac.subtract is that + # net.mac.diff will always return an integer. + diff = minuend: subtrahend: + let + function = "net.mac.diff"; + minuend' = typechecks.mac function "minuend" minuend; + subtrahend' = typechecks.mac function "subtrahend" subtrahend; + in + implementations.mac.diff minuend' subtrahend'; + + # subtract :: (ip | mac | integer) -> mac -> mac + # + # net.mac.subtract is also the reverse of net.ip.add: + # + # net.mac.subtract a (net.mac.add a b) = b + # net.mac.subtract b (net.mac.add a b) = a + # + # The difference between net.mac.subtract and net.mac.diff is that + # net.mac.subtract will always return a MAC address. + subtract = delta: mac: + let + function = "net.mac.subtract"; + delta' = typechecks.numeric function "delta" delta; + mac' = typechecks.mac function "mac" mac; + in + builders.mac (implementations.mac.subtract delta' mac'); + }; + + cidr = { + # add :: (ip | mac | integer) -> cidr -> cidr + # + # > net.cidr.add 2 "127.0.0.0/8" + # "129.0.0.0/8" + # + # > net.cidr.add (-2) "127.0.0.0/8" + # "125.0.0.0/8" + add = delta: cidr: + let + function = "net.cidr.add"; + delta' = typechecks.numeric function "delta" delta; + cidr' = typechecks.cidr function "cidr" cidr; + in + builders.cidr (implementations.cidr.add delta' cidr'); + + # child :: cidr -> cidr -> bool + # + # > net.cidr.child "10.10.10.0/24" "10.0.0.0/8" + # true + # + # > net.cidr.child "127.0.0.0/8" "10.0.0.0/8" + # false + child = subcidr: cidr: + let + function = "net.cidr.child"; + subcidr' = typechecks.cidr function "subcidr" subcidr; + cidr' = typechecks.cidr function "cidr" cidr; + in + implementations.cidr.child subcidr' cidr'; + + # contains :: ip -> cidr -> bool + # + # > net.cidr.contains "127.0.0.1" "127.0.0.0/8" + # true + # + # > net.cidr.contains "127.0.0.1" "192.168.0.0/16" + # false + contains = ip: cidr: + let + function = "net.cidr.contains"; + ip' = typechecks.ip function "ip" ip; + cidr' = typechecks.cidr function "cidr" cidr; + in + implementations.cidr.contains ip' cidr'; + + # capacity :: cidr -> integer + # + # > net.cidr.capacity "172.16.0.0/12" + # 1048576 + # + # > net.cidr.capacity "dead:cafe:beef::/96" + # 4294967296 + # + # > net.cidr.capacity "dead:cafe:beef::/48" (saturates to maxBound) + # 9223372036854775807 + capacity = cidr: + let + function = "net.cidr.capacity"; + cidr' = typechecks.cidr function "cidr" cidr; + in + implementations.cidr.capacity cidr'; + + # host :: (ip | mac | integer) -> cidr -> ip + # + # > net.cidr.host 10000 "10.0.0.0/8" + # 10.0.39.16 + # + # > net.cidr.host 10000 "dead:cafe:beef::/64" + # "dead:cafe:beef::2710" + # + # net.cidr.host "127.0.0.1" "dead:cafe:beef::/48" + # > "dead:cafe:beef::7f00:1" + # + # Inpsired by: + # https://www.terraform.io/docs/configuration/functions/cidrhost.html + host = hostnum: cidr: + let + function = "net.cidr.host"; + hostnum' = typechecks.numeric function "hostnum" hostnum; + cidr' = typechecks.cidr function "cidr" cidr; + in + builders.ip (implementations.cidr.host hostnum' cidr'); + + # length :: cidr -> integer + # + # > net.cidr.prefix "127.0.0.0/8" + # 8 + # + # > net.cidr.prefix "dead:cafe:beef::/48" + # 48 + length = cidr: + let + function = "net.cidr.length"; + cidr' = typechecks.cidr function "cidr" cidr; + in + implementations.cidr.length cidr'; + + # make :: integer -> ip -> cidr + # + # > net.cidr.make 24 "192.168.0.150" + # "192.168.0.0/24" + # + # > net.cidr.make 40 "dead:cafe:beef::feed:face:f00d" + # "dead:cafe:be00::/40" + make = length: base: + let + function = "net.cidr.make"; + length' = typechecks.int function "length" length; + base' = typechecks.ip function "base" base; + in + builders.cidr (implementations.cidr.make length' base'); + + # netmask :: cidr -> ip + # + # > net.cidr.netmask "192.168.0.0/24" + # "255.255.255.0" + # + # > net.cidr.netmask "dead:cafe:beef::/64" + # "ffff:ffff:ffff:ffff::" + netmask = cidr: + let + function = "net.cidr.netmask"; + cidr' = typechecks.cidr function "cidr" cidr; + in + builders.ip (implementations.cidr.netmask cidr'); + + # size :: cidr -> integer + # + # > net.cidr.prefix "127.0.0.0/8" + # 24 + # + # > net.cidr.prefix "dead:cafe:beef::/48" + # 80 + size = cidr: + let + function = "net.cidr.size"; + cidr' = typechecks.cidr function "cidr" cidr; + in + implementations.cidr.size cidr'; + + # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr + # + # > net.cidr.subnet 4 2 "172.16.0.0/12" + # "172.18.0.0/16" + # + # > net.cidr.subnet 4 15 "10.1.2.0/24" + # "10.1.2.240/28" + # + # > net.cidr.subnet 16 162 "fd00:fd12:3456:7890::/56" + # "fd00:fd12:3456:7800:a200::/72" + # + # Inspired by: + # https://www.terraform.io/docs/configuration/functions/cidrsubnet.html + subnet = length: netnum: cidr: + let + function = "net.cidr.subnet"; + length' = typechecks.int function "length" length; + netnum' = typechecks.numeric function "netnum" netnum; + cidr' = typechecks.cidr function "cidr" cidr; + in + builders.cidr (implementations.cidr.subnet length' netnum' cidr'); + + }; + } // ({ + types = + let + + mkParsedOptionType = { name, description, parser, builder }: + let + normalize = def: def // { + value = builder (parser def.value); + }; + in + lib.mkOptionType { + inherit name description; + check = x: builtins.isString x && parser x != null; + merge = loc: defs: lib.mergeEqualOption loc (map normalize defs); + }; + + dependent-ip = type: cidr: + let + cidrs = + if builtins.isList cidr + then cidr + else [ cidr ]; + in + lib.types.addCheck type (i: lib.any (net.cidr.contains i) cidrs) // { + description = type.description + " in ${builtins.concatStringsSep " or " cidrs}"; + }; + + dependent-cidr = type: cidr: + let + cidrs = + if builtins.isList cidr + then cidr + else [ cidr ]; + in + lib.types.addCheck type (i: lib.any (net.cidr.child i) cidrs) // { + description = type.description + " in ${builtins.concatStringsSep " or " cidrs}"; + }; + + in + rec { + + ip = mkParsedOptionType { + name = "ip"; + description = "IPv4 or IPv6 address"; + parser = parsers.ip; + builder = builders.ip; + }; + + ip-in = dependent-ip ip; + + ipv4 = mkParsedOptionType { + name = "ipv4"; + description = "IPv4 address"; + parser = parsers.ipv4; + builder = builders.ipv4; + }; + + ipv4-in = dependent-ip ipv4; + + ipv6 = mkParsedOptionType { + name = "ipv6"; + description = "IPv6 address"; + parser = parsers.ipv6; + builder = builders.ipv6; + }; + + ipv6-in = dependent-ip ipv6; + + cidr = mkParsedOptionType { + name = "cidr"; + description = "IPv4 or IPv6 address range in CIDR notation"; + parser = parsers.cidr; + builder = builders.cidr; + }; + + cidr-in = dependent-cidr cidr; + + cidrv4 = mkParsedOptionType { + name = "cidrv4"; + description = "IPv4 address range in CIDR notation"; + parser = parsers.cidrv4; + builder = builders.cidrv4; + }; + + cidrv4-in = dependent-cidr cidrv4; + + cidrv6 = mkParsedOptionType { + name = "cidrv6"; + description = "IPv6 address range in CIDR notation"; + parser = parsers.cidrv6; + builder = builders.cidrv6; + }; + + cidrv6-in = dependent-cidr cidrv6; + + mac = mkParsedOptionType { + name = "mac"; + description = "MAC address"; + parser = parsers.mac; + builder = builders.mac; + }; + + }; + } + ); + + list = { + cons = a: b: [ a ] ++ b; + }; + + bit = + let + shift = n: x: + if n < 0 + then x * math.pow 2 (-n) + else + let + safeDiv = n: d: if d == 0 then 0 else n / d; + d = math.pow 2 n; + in + if x < 0 + then not (safeDiv (not x) d) + else safeDiv x d; + + left = n: shift (-n); + + right = shift; + + and = builtins.bitAnd; + + or = builtins.bitOr; + + xor = builtins.bitXor; + + not = xor (-1); + + mask = n: and (left n 1 - 1); + in + { + inherit left right and or xor not mask; + }; + + math = rec { + max = a: b: + if a > b + then a + else b; + + min = a: b: + if a < b + then a + else b; + + clamp = a: b: c: max a (min b c); + + pow = x: n: + if n == 0 + then 1 + else if bit.and n 1 != 0 + then x * pow (x * x) ((n - 1) / 2) + else pow (x * x) (n / 2); + }; + + parsers = + let + + # fmap :: (a -> b) -> parser a -> parser b + fmap = f: ma: bind ma (a: pure (f a)); + + # pure :: a -> parser a + pure = a: string: { + leftovers = string; + result = a; + }; + + # liftA2 :: (a -> b -> c) -> parser a -> parser b -> parser c + liftA2 = f: ma: mb: bind ma (a: bind mb (b: pure (f a b))); + liftA3 = f: a: b: ap (liftA2 f a b); + liftA4 = f: a: b: c: ap (liftA3 f a b c); + liftA5 = f: a: b: c: d: ap (liftA4 f a b c d); + liftA6 = f: a: b: c: d: e: ap (liftA5 f a b c d e); + + # ap :: parser (a -> b) -> parser a -> parser b + ap = liftA2 (a: a); + + # then_ :: parser a -> parser b -> parser b + then_ = liftA2 (a: b: b); + + # empty :: parser a + empty = string: null; + + # alt :: parser a -> parser a -> parser a + alt = left: right: string: + let + result = left string; + in + if builtins.isNull result + then right string + else result; + + # guard :: bool -> parser {} + guard = condition: if condition then pure { } else empty; + + # mfilter :: (a -> bool) -> parser a -> parser a + mfilter = f: parser: bind parser (a: then_ (guard (f a)) (pure a)); + + # some :: parser a -> parser [a] + some = v: liftA2 list.cons v (many v); + + # many :: parser a -> parser [a] + many = v: alt (some v) (pure [ ]); + + # bind :: parser a -> (a -> parser b) -> parser b + bind = parser: f: string: + let + a = parser string; + in + if builtins.isNull a + then null + else f a.result a.leftovers; + + # run :: parser a -> string -> maybe a + run = parser: string: + let + result = parser string; + in + if builtins.isNull result || result.leftovers != "" + then null + else result.result; + + next = string: + if string == "" + then null + else { + leftovers = builtins.substring 1 (-1) string; + result = builtins.substring 0 1 string; + }; + + # Count how many characters were consumed by a parser + count = parser: string: + let + result = parser string; + in + if builtins.isNull result + then null + else result // { + result = { + inherit (result) result; + count = with result; + builtins.stringLength string - builtins.stringLength leftovers; + }; + }; + + # Limit the parser to n characters at most + limit = n: parser: + fmap (a: a.result) (mfilter (a: a.count <= n) (count parser)); + + # Ensure the parser consumes exactly n characters + exactly = n: parser: + fmap (a: a.result) (mfilter (a: a.count == n) (count parser)); + + char = c: bind next (c': guard (c == c')); + + string = css: + if css == "" + then pure { } + else + let + c = builtins.substring 0 1 css; + cs = builtins.substring 1 (-1) css; + in + then_ (char c) (string cs); + + digit = set: bind next ( + c: then_ + (guard (builtins.hasAttr c set)) + (pure (builtins.getAttr c set)) + ); + + decimalDigits = { + "0" = 0; + "1" = 1; + "2" = 2; + "3" = 3; + "4" = 4; + "5" = 5; + "6" = 6; + "7" = 7; + "8" = 8; + "9" = 9; + }; + + hexadecimalDigits = decimalDigits // { + "a" = 10; + "b" = 11; + "c" = 12; + "d" = 13; + "e" = 14; + "f" = 15; + "A" = 10; + "B" = 11; + "C" = 12; + "D" = 13; + "E" = 14; + "F" = 15; + }; + + fromDecimalDigits = builtins.foldl' (a: c: a * 10 + c) 0; + fromHexadecimalDigits = builtins.foldl' (a: bit.or (bit.left 4 a)) 0; + + # disallow leading zeros + decimal = bind (digit decimalDigits) ( + n: + if n == 0 + then pure 0 + else + fmap + (ns: fromDecimalDigits (list.cons n ns)) + (many (digit decimalDigits)) + ); + + hexadecimal = fmap fromHexadecimalDigits (some (digit hexadecimalDigits)); + + ipv4 = + let + dot = char "."; + + octet = mfilter (n: n < 256) decimal; + + octet' = then_ dot octet; + + fromOctets = a: b: c: d: { + ipv4 = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d; + }; + in + liftA4 fromOctets octet octet' octet' octet'; + + # This is more or less a literal translation of + # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#parser + ipv6 = + let + colon = char ":"; + + hextet = limit 4 hexadecimal; + + fromHextets = hextets: + if builtins.length hextets != 8 + then empty + else + let + a = builtins.elemAt hextets 0; + b = builtins.elemAt hextets 1; + c = builtins.elemAt hextets 2; + d = builtins.elemAt hextets 3; + e = builtins.elemAt hextets 4; + f = builtins.elemAt hextets 5; + g = builtins.elemAt hextets 6; + h = builtins.elemAt hextets 7; + in + pure { + ipv6 = { + a = bit.or (bit.left 16 a) b; + b = bit.or (bit.left 16 c) d; + c = bit.or (bit.left 16 e) f; + d = bit.or (bit.left 16 g) h; + }; + }; + + ipv4' = fmap + ( + address: + let + upper = bit.right 16 address.ipv4; + lower = bit.mask 16 address.ipv4; + in + [ upper lower ] + ) + ipv4; + + part = n: + let + n' = n + 1; + hex = liftA2 list.cons hextet + ( + then_ colon + ( + alt + (then_ colon (doubleColon n')) + (part n') + ) + ); + in + if n == 7 + then fmap (a: [ a ]) hextet + else + if n == 6 + then alt ipv4' hex + else hex; + + doubleColon = n: + bind (alt afterDoubleColon (pure [ ])) ( + rest: + let + missing = 8 - n - builtins.length rest; + in + if missing < 0 + then empty + else pure (builtins.genList (_: 0) missing ++ rest) + ); + + afterDoubleColon = + alt ipv4' + ( + liftA2 list.cons hextet + ( + alt + (then_ colon afterDoubleColon) + (pure [ ]) + ) + ); + + in + bind + ( + alt + ( + then_ + (string "::") + (doubleColon 0) + ) + (part 0) + ) + fromHextets; + + cidrv4 = + liftA2 + (base: length: implementations.cidr.make length base) + ipv4 + (then_ (char "/") (mfilter (n: n <= 32) decimal)); + + cidrv6 = + liftA2 + (base: length: implementations.cidr.make length base) + ipv6 + (then_ (char "/") (mfilter (n: n <= 128) decimal)); + + mac = + let + colon = char ":"; + + octet = exactly 2 hexadecimal; + + octet' = then_ colon octet; + + fromOctets = a: b: c: d: e: f: { + mac = bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 (bit.or (bit.left 8 a) b)) c)) d)) e)) f; + }; + in + liftA6 fromOctets octet octet' octet' octet' octet' octet'; + + in + { + ipv4 = run ipv4; + ipv6 = run ipv6; + ip = run (alt ipv4 ipv6); + cidrv4 = run cidrv4; + cidrv6 = run cidrv6; + cidr = run (alt cidrv4 cidrv6); + mac = run mac; + numeric = run (alt (alt ipv4 ipv6) mac); + }; + + builders = + let + + ipv4 = address: + let + abcd = address.ipv4; + abc = bit.right 8 abcd; + ab = bit.right 8 abc; + a = bit.right 8 ab; + b = bit.mask 8 ab; + c = bit.mask 8 abc; + d = bit.mask 8 abcd; + in + builtins.concatStringsSep "." (map toString [ a b c d ]); + + # This is more or less a literal translation of + # https://hackage.haskell.org/package/ip/docs/src/Net.IPv6.html#encode + ipv6 = address: + let + + digits = "0123456789abcdef"; + + toHexString = n: + let + rest = bit.right 4 n; + current = bit.mask 4 n; + prefix = + if rest == 0 + then "" + else toHexString rest; + in + "${prefix}${builtins.substring current 1 digits}"; + + in + if (with address.ipv6; a == 0 && b == 0 && c == 0 && d > 65535) + then "::${ipv4 { ipv4 = address.ipv6.d; }}" + else + if (with address.ipv6; a == 0 && b == 0 && c == 65535) + then "::ffff:${ipv4 { ipv4 = address.ipv6.d; }}" + else + let + + a = bit.right 16 address.ipv6.a; + b = bit.mask 16 address.ipv6.a; + c = bit.right 16 address.ipv6.b; + d = bit.mask 16 address.ipv6.b; + e = bit.right 16 address.ipv6.c; + f = bit.mask 16 address.ipv6.c; + g = bit.right 16 address.ipv6.d; + h = bit.mask 16 address.ipv6.d; + + hextets = [ a b c d e f g h ]; + + # calculate the position and size of the longest sequence of + # zeroes within the list of hextets + longest = + let + go = i: current: best: + if i < builtins.length hextets + then + let + n = builtins.elemAt hextets i; + + current' = + if n == 0 + then + if builtins.isNull current + then { + size = 1; + position = i; + } + else current // { + size = current.size + 1; + } + else null; + + best' = + if n == 0 + then + if builtins.isNull best + then current' + else + if current'.size > best.size + then current' + else best + else best; + in + go (i + 1) current' best' + else best; + in + go 0 null null; + + format = hextets: + builtins.concatStringsSep ":" (map toHexString hextets); + in + if builtins.isNull longest + then format hextets + else + let + sublist = i: length: xs: + map + (builtins.elemAt xs) + (builtins.genList (x: x + i) length); + + end = longest.position + longest.size; + + before = sublist 0 longest.position hextets; + + after = sublist end (builtins.length hextets - end) hextets; + in + "${format before}::${format after}"; + + ip = address: + if address ? ipv4 + then ipv4 address + else ipv6 address; + + cidrv4 = cidr: + "${ipv4 cidr.base}/${toString cidr.length}"; + + cidrv6 = cidr: + "${ipv6 cidr.base}/${toString cidr.length}"; + + cidr = cidr: + "${ip cidr.base}/${toString cidr.length}"; + + mac = address: + let + digits = "0123456789abcdef"; + octet = n: + let + upper = bit.right 4 n; + lower = bit.mask 4 n; + in + "${builtins.substring upper 1 digits}${builtins.substring lower 1 digits}"; + in + let + a = bit.mask 8 (bit.right 40 address.mac); + b = bit.mask 8 (bit.right 32 address.mac); + c = bit.mask 8 (bit.right 24 address.mac); + d = bit.mask 8 (bit.right 16 address.mac); + e = bit.mask 8 (bit.right 8 address.mac); + f = bit.mask 8 (bit.right 0 address.mac); + in + "${octet a}:${octet b}:${octet c}:${octet d}:${octet e}:${octet f}"; + + in + { + inherit ipv4 ipv6 ip cidrv4 cidrv6 cidr mac; + }; + + arithmetic = rec { + # or :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) + or = a_: b: + let + a = coerce b a_; + in + if a ? ipv6 + then { + ipv6 = { + a = bit.or a.ipv6.a b.ipv6.a; + b = bit.or a.ipv6.b b.ipv6.b; + c = bit.or a.ipv6.c b.ipv6.c; + d = bit.or a.ipv6.d b.ipv6.d; + }; + } + else if a ? ipv4 + then { + ipv4 = bit.or a.ipv4 b.ipv4; + } + else if a ? mac + then { + mac = bit.or a.mac b.mac; + } + else bit.or a b; + + # and :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) + and = a_: b: + let + a = coerce b a_; + in + if a ? ipv6 + then { + ipv6 = { + a = bit.and a.ipv6.a b.ipv6.a; + b = bit.and a.ipv6.b b.ipv6.b; + c = bit.and a.ipv6.c b.ipv6.c; + d = bit.and a.ipv6.d b.ipv6.d; + }; + } + else if a ? ipv4 + then { + ipv4 = bit.and a.ipv4 b.ipv4; + } + else if a ? mac + then { + mac = bit.and a.mac b.mac; + } + else bit.and a b; + + # not :: (ip | mac | integer) -> (ip | mac | integer) + not = a: + if a ? ipv6 + then { + ipv6 = { + a = bit.mask 32 (bit.not a.ipv6.a); + b = bit.mask 32 (bit.not a.ipv6.b); + c = bit.mask 32 (bit.not a.ipv6.c); + d = bit.mask 32 (bit.not a.ipv6.d); + }; + } + else if a ? ipv4 + then { + ipv4 = bit.mask 32 (bit.not a.ipv4); + } + else if a ? mac + then { + mac = bit.mask 48 (bit.not a.mac); + } + else bit.not a; + + # add :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) + add = + let + split = a: { + fst = bit.mask 32 (bit.right 32 a); + snd = bit.mask 32 a; + }; + in + a_: b: + let + a = coerce b a_; + in + if a ? ipv6 + then + let + a' = split (a.ipv6.a + b.ipv6.a + b'.fst); + b' = split (a.ipv6.b + b.ipv6.b + c'.fst); + c' = split (a.ipv6.c + b.ipv6.c + d'.fst); + d' = split (a.ipv6.d + b.ipv6.d); + in + { + ipv6 = { + a = a'.snd; + b = b'.snd; + c = c'.snd; + d = d'.snd; + }; + } + else if a ? ipv4 + then { + ipv4 = bit.mask 32 (a.ipv4 + b.ipv4); + } + else if a ? mac + then { + mac = bit.mask 48 (a.mac + b.mac); + } + else a + b; + + # subtract :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) + subtract = a: b: add (add 1 (not (coerce b a))) b; + + # diff :: (ip | mac | integer) -> (ip | mac | integer) -> (ipv6 | integer) + diff = a: b: + let + toIPv6 = coerce ({ ipv6.a = 0; }); + result = (subtract b (toIPv6 a)).ipv6; + max32 = bit.left 32 1 - 1; + in + if result.a == 0 && result.b == 0 && bit.right 31 result.c == 0 || result.a == max32 && result.b == max32 && bit.right 31 result.c == 1 + then bit.or (bit.left 32 result.c) result.d + else { + ipv6 = result; + }; + + # left :: integer -> (ip | mac | integer) -> (ip | mac | integer) + left = i: right (-i); + + # right :: integer -> (ip | mac | integer) -> (ip | mac | integer) + right = + let + step = i: x: { + _1 = bit.mask 32 (bit.right (i + 96) x); + _2 = bit.mask 32 (bit.right (i + 64) x); + _3 = bit.mask 32 (bit.right (i + 32) x); + _4 = bit.mask 32 (bit.right i x); + _5 = bit.mask 32 (bit.right (i - 32) x); + _6 = bit.mask 32 (bit.right (i - 64) x); + _7 = bit.mask 32 (bit.right (i - 96) x); + }; + ors = builtins.foldl' bit.or 0; + in + i: x: + if x ? ipv6 + then + let + a' = step i x.ipv6.a; + b' = step i x.ipv6.b; + c' = step i x.ipv6.c; + d' = step i x.ipv6.d; + in + { + ipv6 = { + a = ors [ a'._4 b'._3 c'._2 d'._1 ]; + b = ors [ a'._5 b'._4 c'._3 d'._2 ]; + c = ors [ a'._6 b'._5 c'._4 d'._3 ]; + d = ors [ a'._7 b'._6 c'._5 d'._4 ]; + }; + } + else if x ? ipv4 + then { + ipv4 = bit.mask 32 (bit.right i x.ipv4); + } + else if x ? mac + then { + mac = bit.mask 48 (bit.right i x.mac); + } + else bit.right i x; + + # shadow :: integer -> (ip | mac | integer) -> (ip | mac | integer) + shadow = n: a: and (right n (left n (coerce a (-1)))) a; + + # coshadow :: integer -> (ip | mac | integer) -> (ip | mac | integer) + coshadow = n: a: and (not (right n (left n (coerce a (-1))))) a; + + # coerce :: (ip | mac | integer) -> (ip | mac | integer) -> (ip | mac | integer) + coerce = target: value: + if target ? ipv6 + then + if value ? ipv6 + then value + else if value ? ipv4 + then { + ipv6 = { + a = 0; + b = 0; + c = 0; + d = value.ipv4; + }; + } + else if value ? mac + then { + ipv6 = { + a = 0; + b = 0; + c = bit.right 32 value.mac; + d = bit.mask 32 value.mac; + }; + } + else { + ipv6 = { + a = bit.mask 32 (bit.right 96 value); + b = bit.mask 32 (bit.right 64 value); + c = bit.mask 32 (bit.right 32 value); + d = bit.mask 32 value; + }; + } + else if target ? ipv4 + then + if value ? ipv6 + then { + ipv4 = value.ipv6.d; + } + else if value ? ipv4 + then value + else if value ? mac + then { + ipv4 = bit.mask 32 value.mac; + } + else { + ipv4 = bit.mask 32 value; + } + else if target ? mac + then + if value ? ipv6 + then { + mac = bit.or (bit.left 32 (bit.mask 16 value.ipv6.c)) value.ipv6.d; + } + else if value ? ipv4 + then { + mac = value.ipv4; + } + else if value ? mac + then value + else { + mac = bit.mask 48 value; + } + else + if value ? ipv6 + then + builtins.foldl' bit.or 0 + [ + (bit.left 96 value.ipv6.a) + (bit.left 64 value.ipv6.b) + (bit.left 32 value.ipv6.c) + value.ipv6.d + ] + else if value ? ipv4 + then value.ipv4 + else if value ? mac + then value.mac + else value; + }; + + implementations = { + ip = { + # add :: (ip | mac | integer) -> ip -> ip + add = arithmetic.add; + + # diff :: ip -> ip -> (ipv6 | integer) + diff = arithmetic.diff; + + # subtract :: (ip | mac | integer) -> ip -> ip + subtract = arithmetic.subtract; + }; + + mac = { + # add :: (ip | mac | integer) -> mac -> mac + add = arithmetic.add; + + # diff :: mac -> mac -> (ipv6 | integer) + diff = arithmetic.diff; + + # subtract :: (ip | mac | integer) -> mac -> mac + subtract = arithmetic.subtract; + }; + + cidr = rec { + # add :: (ip | mac | integer) -> cidr -> cidr + add = delta: cidr: + let + size' = size cidr; + in + { + base = arithmetic.left size' (arithmetic.add delta (arithmetic.right size' cidr.base)); + inherit (cidr) length; + }; + + # capacity :: cidr -> integer + capacity = cidr: + let + size' = size cidr; + in + if size' > 62 + then 9223372036854775807 # maxBound to prevent overflow + else bit.left size' 1; + + # child :: cidr -> cidr -> bool + child = subcidr: cidr: + length subcidr > length cidr && contains (host 0 subcidr) cidr; + + # contains :: ip -> cidr -> bool + contains = ip: cidr: host 0 (make cidr.length ip) == host 0 cidr; + + # host :: (ip | mac | integer) -> cidr -> ip + host = index: cidr: + let + index' = arithmetic.coerce cidr.base index; + in + arithmetic.or (arithmetic.shadow cidr.length index') cidr.base; + + # length :: cidr -> integer + length = cidr: cidr.length; + + # netmask :: cidr -> ip + netmask = cidr: arithmetic.coshadow cidr.length (arithmetic.coerce cidr.base (-1)); + + # size :: cidr -> integer + size = cidr: (if cidr.base ? ipv6 then 128 else 32) - cidr.length; + + # subnet :: integer -> (ip | mac | integer) -> cidr -> cidr + subnet = length: index: cidr: + let + length' = cidr.length + length; + index' = arithmetic.coerce cidr.base index; + size = (if cidr.base ? ipv6 then 128 else 32) - length'; + in + make length' (host (arithmetic.left size index') cidr); + + # make :: integer -> ip -> cidr + make = length: base: + let + length' = math.clamp 0 (if base ? ipv6 then 128 else 32) length; + in + { + base = arithmetic.coshadow length' base; + length = length'; + }; + }; + }; + + typechecks = + let + + fail = description: function: argument: + builtins.throw "${function}: ${argument} parameter must be ${description}"; + + meta = parser: description: function: argument: input: + let + error = fail description function argument; + in + if !builtins.isString input + then error + else + let + result = parser input; + in + if builtins.isNull result + then error + else result; + + in + { + int = function: argument: input: + if builtins.isInt input + then input + else fail "an integer" function argument; + ip = meta parsers.ip "an IPv4 or IPv6 address"; + cidr = meta parsers.cidr "an IPv4 or IPv6 address range in CIDR notation"; + mac = meta parsers.mac "a MAC address"; + numeric = function: argument: input: + if builtins.isInt input + then input + else meta parsers.numeric "an integer or IPv4, IPv6 or MAC address" function argument input; + }; + +in +net diff --git a/flake.nix b/flake.nix index 25e1310..379106d 100644 --- a/flake.nix +++ b/flake.nix @@ -70,6 +70,8 @@ ./flake-parts/nixos.nix ./flake-parts/kubenix.nix ./flake-parts/shell.nix + ./flake-parts/utils + ./flake-parts/machines ] // (flake-utils.lib.eachDefaultSystem (system: { formatter = nixpkgs.legacyPackages.${system}.nixfmt; }));