Files
nilsandClaude Sonnet 5.5 c395a43a78 Add a static ULA per LAN, announced via SLAAC and RDNSS
lans.<n>.ipv6.address (a ULA /64) is assigned to the LAN, announced
with an explicit prefix (networkd only announces the delegated prefix
when prefix delegation is on) and advertised as RDNSS; Unbound listens
and answers on it. Checked at build time: valid IPv6 CIDR, /64, fc00::/7.

Covered by the onestick VM test (client SLAAC address, ping and DNS over
the ULA, RA captured on the wire), assertion cases and a unit test for
the /64 prefix helper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:48:47 +01:00

82 lines
2.6 KiB
Nix

# Small IPv4 helpers (addresses are plain 32-bit integers inside Nix).
# Parsing returns null on malformed input so callers can turn that into an
# assertion message instead of an evaluation crash.
{ lib }:
let
pow2 = n: if n == 0 then 1 else 2 * pow2 (n - 1);
in
rec {
ipv4 = rec {
parse =
s:
let
m = builtins.match "([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})" s;
octets = map lib.toInt m;
in
if m == null || lib.any (o: o > 255) octets then
null
else
lib.foldl' (acc: o: acc * 256 + o) 0 octets;
# "10.0.0.5" or "10.0.0.0/24" -> { address (int); prefixLength }, or null.
parseNet =
s:
let
m = builtins.match "([0-9.]+)(/([0-9]{1,2}))?" s;
address = if m == null then null else parse (builtins.elemAt m 0);
prefixLength =
if m == null || builtins.elemAt m 2 == null then 32 else lib.toInt (builtins.elemAt m 2);
in
if m == null || address == null || prefixLength > 32 then
null
else
{ inherit address prefixLength; };
format =
n:
lib.concatMapStringsSep "." (shift: toString (lib.mod (n / pow2 shift) 256)) [
24
16
8
0
];
netMask = prefixLength: 4294967295 - (pow2 (32 - prefixLength) - 1);
# Network address (integer) of `address` under `prefixLength`.
networkOf = address: prefixLength: builtins.bitAnd address (netMask prefixLength);
# "192.168.10.1" /24 -> "192.168.10.0/24"
cidr =
address: prefixLength:
"${format (networkOf (parse address) prefixLength)}/${toString prefixLength}";
inSubnet =
{ address, prefixLength }: ip: networkOf ip prefixLength == networkOf address prefixLength;
# Whether two subnets share any address (one contains the other's network).
overlaps =
a: b:
let
p = lib.min a.prefixLength b.prefixLength;
in
networkOf a.address p == networkOf b.address p;
};
ipv6 = {
# The first 64 bits of an address (any form, with or without a prefix
# length) as a /64 prefix, e.g. fd2c:cdef:cde9::1/64 -> fd2c:cdef:cde9:0::/64.
prefix64 =
address:
let
bare = builtins.head (lib.splitString "/" address);
halves = lib.splitString "::" bare;
groups = s: lib.filter (g: g != "") (lib.splitString ":" s);
head = groups (builtins.elemAt halves 0);
tail = lib.optionals (lib.length halves > 1) (groups (builtins.elemAt halves 1));
full = head ++ lib.replicate (8 - lib.length head - lib.length tail) "0" ++ tail;
in
"${lib.concatStringsSep ":" (lib.take 4 full)}::/64";
};
}