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:
@@ -78,13 +78,46 @@ in
|
||||
description = "LAN segments served by this router, keyed by name.";
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ config, ... }:
|
||||
{ name, config, ... }:
|
||||
{
|
||||
options = {
|
||||
interface = mkOption {
|
||||
type = types.str;
|
||||
example = "eth1";
|
||||
description = "Interface (or bridge/VLAN device) carrying this LAN.";
|
||||
description = ''
|
||||
Interface carrying this LAN. With `vlan` set this is the
|
||||
parent (trunk) interface on which the VLAN device is created.
|
||||
'';
|
||||
};
|
||||
|
||||
vlan = mkOption {
|
||||
type = types.nullOr (types.ints.between 1 4094);
|
||||
default = null;
|
||||
description = "802.1Q VLAN ID, or null for an untagged LAN on `interface`.";
|
||||
};
|
||||
|
||||
device = mkOption {
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
description = "Device the LAN's addresses live on (derived).";
|
||||
};
|
||||
|
||||
zone = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = ''
|
||||
Firewall zone this LAN belongs to. LANs sharing a zone may
|
||||
talk to each other freely; traffic between zones is denied
|
||||
unless a rule in `router.firewall.rules` allows it. Defaults
|
||||
to a zone named after the LAN.
|
||||
'';
|
||||
};
|
||||
|
||||
internet = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether this LAN may reach the internet (opt-in).";
|
||||
};
|
||||
|
||||
ipv4 = {
|
||||
@@ -139,6 +172,9 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config.device =
|
||||
if config.vlan == null then config.interface else "${config.interface}.${toString config.vlan}";
|
||||
|
||||
# "invalid" is only ever seen when assertions.nix is already failing.
|
||||
config.ipv4.subnet =
|
||||
if rlib.ipv4.parse config.ipv4.address == null then
|
||||
@@ -150,6 +186,52 @@ in
|
||||
);
|
||||
};
|
||||
|
||||
firewall.rules = mkOption {
|
||||
default = [ ];
|
||||
description = ''
|
||||
Allow rules between zones, on top of the default (deny between zones).
|
||||
Rules are one-directional; return traffic is allowed by connection
|
||||
tracking. Both ends must be zones of LANs defined in `router.lans`.
|
||||
'';
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
options = {
|
||||
from = mkOption {
|
||||
type = types.str;
|
||||
description = "Source zone.";
|
||||
};
|
||||
to = mkOption {
|
||||
type = types.str;
|
||||
description = "Destination zone.";
|
||||
};
|
||||
protocol = mkOption {
|
||||
type = types.enum [
|
||||
"any"
|
||||
"tcp"
|
||||
"udp"
|
||||
"icmp"
|
||||
];
|
||||
default = "any";
|
||||
};
|
||||
ports = mkOption {
|
||||
type = types.listOf types.port;
|
||||
default = [ ];
|
||||
description = "Destination ports (tcp/udp only); empty means all ports.";
|
||||
};
|
||||
destination = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "192.168.10.5";
|
||||
description = ''
|
||||
Restrict the rule to an IPv4 address or CIDR inside the
|
||||
destination zone; null means the whole zone.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
dns.forwarders = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
|
||||
Reference in New Issue
Block a user