Add VLANs, firewall zones and inter-zone rules
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>
This commit is contained in:
@@ -5,6 +5,17 @@ let
|
||||
pppoe = cfg.wan.ipv4.type == "pppoe";
|
||||
vlan = cfg.wan.ipv4.pppoe.vlan;
|
||||
vlanDevice = "${cfg.wan.interface}.${toString vlan}";
|
||||
|
||||
lans = lib.attrValues cfg.lans;
|
||||
vlanLans = lib.filter (l: l.vlan != null) lans;
|
||||
# VLAN devices hang off their parent; the parent's own unit must list them.
|
||||
vlansOn = parent: map (l: l.device) (lib.filter (l: l.interface == parent) vlanLans);
|
||||
untaggedParents = map (l: l.interface) (lib.filter (l: l.vlan == null) lans);
|
||||
# Parents that carry only tagged LANs still need a unit to be brought up.
|
||||
trunkParents = lib.filter (p: !(lib.elem p untaggedParents)) (
|
||||
lib.unique (map (l: l.interface) vlanLans)
|
||||
);
|
||||
|
||||
dhcp = if pppoe then (if v6 then "ipv6" else "no") else (if v6 then "yes" else "ipv4");
|
||||
in
|
||||
{
|
||||
@@ -17,15 +28,26 @@ in
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
};
|
||||
|
||||
systemd.network.netdevs = lib.optionalAttrs (pppoe && vlan != null) {
|
||||
"05-wan-pppoe" = {
|
||||
netdevConfig = {
|
||||
Kind = "vlan";
|
||||
Name = vlanDevice;
|
||||
systemd.network.netdevs =
|
||||
lib.optionalAttrs (pppoe && vlan != null) {
|
||||
"05-wan-pppoe" = {
|
||||
netdevConfig = {
|
||||
Kind = "vlan";
|
||||
Name = vlanDevice;
|
||||
};
|
||||
vlanConfig.Id = vlan;
|
||||
};
|
||||
vlanConfig.Id = vlan;
|
||||
};
|
||||
};
|
||||
}
|
||||
// lib.mapAttrs' (
|
||||
name: lan:
|
||||
lib.nameValuePair "06-lan-${name}" {
|
||||
netdevConfig = {
|
||||
Kind = "vlan";
|
||||
Name = lan.device;
|
||||
};
|
||||
vlanConfig.Id = lan.vlan;
|
||||
}
|
||||
) (lib.filterAttrs (_: l: l.vlan != null) cfg.lans);
|
||||
|
||||
systemd.network.networks = {
|
||||
"10-wan" = {
|
||||
@@ -68,13 +90,16 @@ in
|
||||
name: lan:
|
||||
lib.nameValuePair "20-lan-${name}" (
|
||||
{
|
||||
matchConfig.Name = lan.interface;
|
||||
matchConfig.Name = lan.device;
|
||||
address = [ "${lan.ipv4.address}/${toString lan.ipv4.prefixLength}" ];
|
||||
networkConfig = {
|
||||
ConfigureWithoutCarrier = true;
|
||||
# Never accept RAs from the inside.
|
||||
IPv6AcceptRA = false;
|
||||
}
|
||||
// lib.optionalAttrs (lan.vlan == null && vlansOn lan.interface != [ ]) {
|
||||
VLAN = vlansOn lan.interface;
|
||||
}
|
||||
// lib.optionalAttrs lan.ipv6.pd {
|
||||
DHCPPrefixDelegation = true;
|
||||
IPv6SendRA = true;
|
||||
@@ -91,6 +116,20 @@ in
|
||||
ipv6SendRAConfig.EmitDNS = false;
|
||||
}
|
||||
)
|
||||
) cfg.lans;
|
||||
) cfg.lans
|
||||
// lib.listToAttrs (
|
||||
map (
|
||||
parent:
|
||||
lib.nameValuePair "19-trunk-${parent}" {
|
||||
matchConfig.Name = parent;
|
||||
networkConfig = {
|
||||
LinkLocalAddressing = "no";
|
||||
IPv6AcceptRA = false;
|
||||
ConfigureWithoutCarrier = true;
|
||||
VLAN = vlansOn parent;
|
||||
};
|
||||
}
|
||||
) trunkParents
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user