diff --git a/lib/default.nix b/lib/default.nix index 5675bad..ca7c9ee 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -1,4 +1,6 @@ {lib}: { + imports=[./net.nix]; + dirToStrings = dir: (map (v: builtins.readFile "${dir}/${v}") (builtins.filter (v: (builtins.readFileType "${dir}/${v}") == "regular") ( diff --git a/lib/net.nix b/lib/net.nix new file mode 100644 index 0000000..702db28 --- /dev/null +++ b/lib/net.nix @@ -0,0 +1,1269 @@ +{lib ? null, ...}: 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'); + }; + } + // ( + if builtins.isNull lib + then {} + else { + 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; + + hextet' = then_ colon hextet; + + 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 { + lib = { + inherit net; + }; +} diff --git a/modules/hostopts.nix b/modules/hostopts.nix index d6519dc..a6aa2e3 100644 --- a/modules/hostopts.nix +++ b/modules/hostopts.nix @@ -14,22 +14,10 @@ type = lib.types.str; description = "Primary human users long name"; }; - gui.enable = lib.mkEnableOption { - description = "enable GUI"; - default = false; - }; - isLaptop = lib.mkEnableOption { - description = "machine is a laptop"; - default = false; - }; - isVm = lib.mkEnableOption { - description = "machine is a virtual machine"; - default = false; - }; - isSever = lib.mkEnableOption { - description = "machine is primarily a server"; - default = false; - }; + gui.enable = lib.mkEnableOption "enable GUI"; + isLaptop = lib.mkEnableOption "machine is a laptop"; + isVm = lib.mkEnableOption "machine is a virtual machine"; + isSever = lib.mkEnableOption "machine is primarily a server"; }; }; } diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix new file mode 100644 index 0000000..4da28bd --- /dev/null +++ b/modules/nixos/default.nix @@ -0,0 +1,9 @@ +{ + config, + pkgs, + lib, + inputs, + ... +}: { + imports=[./systemd-dhcpServ.nix]; +} diff --git a/modules/nixos/systemd-dhcpServ.nix b/modules/nixos/systemd-dhcpServ.nix new file mode 100644 index 0000000..9e85bce --- /dev/null +++ b/modules/nixos/systemd-dhcpServ.nix @@ -0,0 +1,108 @@ +{ + config, + pkgs, + lib, + inputs, + ... +}: { + options = { + host.systemdDhcpSrv = { + enable = lib.mkEnableOption "systemd DHCP server"; + + interface = lib.mkOption { + type = lib.types.str; + description = "interface to run dhcp server on"; + }; + + uplinkInterface = lib.mkOption { + type = lib.types.str; + description = "if dns, router, or ntp are set but no adresses are set, pass on the settings of this interface."; + default = ":auto"; + }; + + pool = lib.mkOption { + description = "the pool of ips the dhcp server will hand out."; + type = lib.types.submodule { + options = { + start = lib.mkOption { + type = lib.types.str; + description = "starting IP of the range the dhcp server will assign"; + }; + end = lib.mkOption { + type = lib.types.str; + description = "ending IP of the range the dhcp server will assign"; + }; + }; + }; + }; + + time = lib.mkOption { + type = lib.types.submodule { + options = { + default = lib.mkOption { + description = "the default dhcp lease time, in seconds, defaults to 1h"; + type = lib.types.nullOr lib.types.int; + default = null; + }; + max = lib.mkOption { + description = "the max dhcp lease time, in seconds. defaults to 12h"; + type = lib.types.nullOr lib.types.int; + default = null; + }; + }; + }; + }; + + dns = lib.mkoption { + type = lib.types.submodule { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "whether to include dns server info in the dhcp lease"; + }; + servers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "IPs of dns servers to hand out"; + default = []; + }; + }; + }; + }; + + router = lib.mkoption { + type = lib.types.submodule { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "whether to include router (gateway) info in the dhcp lease"; + }; + servers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "IPs of dns servers to hand out"; + default = []; + }; + }; + }; + }; + + ntp = lib.mkoption { + type = lib.types.submodule { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "whether to include ntp server info in the dhcp lease"; + }; + servers = lib.mkOption { + type = lib.types.listOf lib.types.str; + description = "IPs of ntp servers to hand out"; + default = []; + }; + }; + }; + }; + }; + }; +}