LANs can sit on an 802.1Q VLAN (`vlan` on a trunk `interface`); networkd creates the device and brings the trunk up. LANs belong to a named `zone` (default: their own name). Traffic inside a zone is forwarded freely, traffic between zones is denied unless a `router.firewall.rules` entry allows it (protocol, ports, optional destination inside the target zone). Internet access is now opt-in per LAN (`internet`, default false), replacing the previous allow-out default. Assertions reject unknown zones, bad destinations, duplicate devices, over-long device names and a WAN trunk. Kea subnet IDs now come from the network address so adding LANs does not renumber them and orphan persistent leases. Tests: a five-VM `vlans` check (untagged + two VLANs, shared zone, one-way rules, per-host destination restriction), new assertion cases, parseNet unit tests, and shared test tooling in tests/common.nix. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
66 lines
2.0 KiB
Nix
66 lines
2.0 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;
|
|
};
|
|
}
|