Skip to content

Instantly share code, notes, and snippets.

@duairc
Last active April 16, 2024 00:28
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save duairc/5c9bb3c922e5d501a1edb9e7b3b845ba to your computer and use it in GitHub Desktop.
Save duairc/5c9bb3c922e5d501a1edb9e7b3b845ba to your computer and use it in GitHub Desktop.
IP address arithmetic and validation in Nix
{ 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;
};
}
@oddlama
Copy link

oddlama commented Apr 20, 2023

FYI I just noticed that the examples for cidr.size in line 286-289 are using cidr.prefix instead of cidr.size. Same for cidr.length.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment