Last active
January 18, 2025 10:45
-
-
Save duairc/5c9bb3c922e5d501a1edb9e7b3b845ba to your computer and use it in GitHub Desktop.
IP address arithmetic and validation in Nix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ 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; | |
}; | |
} |
Would you be interested in exposing this library as a flake so that it can be consumed and used in conjunction with something like lib.extend
for use in downstream projects?
@starcraft66 A couple of months ago I packaged it up as a flake, exposing it as a overlay (with oddlama
's fixes included): https://github.com/reo101/nix-lib-net
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI I just noticed that the examples for cidr.size in line 286-289 are using
cidr.prefix
instead ofcidr.size
. Same for cidr.length.