nixos/services/validate-forwards.nix

42 lines
2.3 KiB
Nix
Raw Permalink Normal View History

# Validate hand-edited TOML as well as UI changes, before generating nft syntax.
{ lib }:
data:
let
fail = message: throw "ports.toml: ${message}";
lanIP = value: builtins.isString value && builtins.match
"10[.]0[.]0[.]([1-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-4])" value != null;
normalize = entry:
let
name = entry.name or "";
proto = entry.protocol or "";
single = entry ? port;
range = entry ? ports;
spec = if single then toString entry.port else entry.ports or "";
validSpec = builtins.isString spec && builtins.match "[0-9]+(-[0-9]+)?" spec != null;
bounds = map lib.toInt (lib.splitString "-" spec);
first = builtins.head bounds;
last = lib.last bounds;
dest = entry.dest or data.dest_default;
in
if !builtins.isString name || builtins.match "[A-Za-z0-9][A-Za-z0-9 _().-]{0,79}" name == null
then fail "invalid forward name"
else if !(lib.elem proto [ "tcp" "udp" "both" ]) then fail "${name}: invalid protocol"
else if single == range || !validSpec then fail "${name}: use one port or one port range"
else if (single && !builtins.isInt entry.port) || first < 1 || last > 65535 || (range && first >= last)
then fail "${name}: invalid port bounds"
else if !lanIP dest then fail "${name}: destination must be a LAN IPv4 address"
else { inherit name first last dest; protos = if proto == "both" then [ "tcp" "udp" ] else [ proto ]; };
entries = map normalize (data.forward or [ ]);
names = map (e: e.name) entries;
overlaps = a: b: a.first <= b.last && b.first <= a.last && lib.intersectLists a.protos b.protos != [ ];
noOverlaps = list: list == [ ] || (
lib.all (b: !overlaps (builtins.head list) b) (builtins.tail list)
&& noOverlaps (builtins.tail list));
reachable = port: lib.any (e: lib.elem "tcp" e.protos && e.first <= port && port <= e.last && e.dest == "10.0.0.1") entries;
in
if (data.dest_default or "") != "10.0.0.1" then fail "dest_default must remain 10.0.0.1"
else if builtins.length names != builtins.length (lib.unique names) then fail "duplicate names"
else if !noOverlaps entries then fail "overlapping protocol/port ranges"
else if !lib.all reachable [ 22 443 ] then fail "TCP 22 and 443 must still reach the router"
else builtins.deepSeq entries data