refactor(nixos): consolidate VLAN networking

This commit was merged in pull request #141.
This commit is contained in:
2026-10-07 21:29:52 -04:00
parent 3a487050fd
commit f173887228
2 changed files with 34 additions and 119 deletions

View File

@@ -15,46 +15,10 @@
hardware.cpu.intel.updateMicrocode = true; hardware.cpu.intel.updateMicrocode = true;
systemd.network = { modules.network.vlans = {
enable = true; interface = "enp2s0";
netdevs = { ids = [ 20 ];
"40-br-vlan20".netdevConfig = {
Name = "br-vlan20";
Kind = "bridge";
}; };
"40-vlan20" = {
netdevConfig = {
Name = "vlan20";
Kind = "vlan";
};
vlanConfig.Id = 20;
};
};
networks = {
"10-uplink" = {
matchConfig = {
Name = "enp2s0";
MACAddress = "78:55:36:07:af:49";
};
networkConfig.DHCP = "yes";
vlan = [ "vlan20" ];
linkConfig.RequiredForOnline = "routable";
};
"40-vlan20" = {
matchConfig.Name = "vlan20";
networkConfig.Bridge = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
"40-br-vlan20" = {
matchConfig.Name = "br-vlan20";
linkConfig.RequiredForOnline = "no";
};
};
};
networking.useDHCP = false;
modules.network.vlans.ids = [ 20 ];
boot.zfs.forceImportRoot = false; boot.zfs.forceImportRoot = false;

View File

@@ -4,24 +4,17 @@
util, util,
... ...
}: }:
# The host networking foundation: per-VLAN bridges over a tagged trunk. # Configures bridges for tagged VLANs on a physical interface that uses DHCP.
let let
cfg = config.modules.network.vlans; # NOLINT(let_order) cfg = config.modules.network.vlans; # NOLINT(let_order)
inherit (util) bridgeName;
vlanName = id: "${cfg.trunk}.${toString id}";
in in
{ {
options.modules.network.vlans = { options.modules.network.vlans = {
enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk"; interface = lib.mkOption {
trunk = lib.mkOption {
type = lib.types.str; type = lib.types.str;
example = "enp1s0"; example = "enp1s0";
description = '' description = ''
Physical interface that carries 802.1Q-tagged traffic for every VLAN. Names the physical interface that carries untagged DHCP traffic and tagged VLANs.
Each declared VLAN attaches to its own bridge.
''; '';
}; };
@@ -34,67 +27,37 @@ in
30 30
]; ];
description = '' description = ''
Tagged VLAN ids to materialize. Creates bridges for the specified tagged VLAN IDs.
Each id N creates one bridge named `br-vlanN`.
'';
};
management = { VLAN ID N creates bridge `br-vlanN`.
vlan = lib.mkOption {
type = lib.types.nullOr (lib.types.ints.between 1 4094);
default = null;
description = ''
VLAN whose bridge carries the host's own management address.
The value must be one of `ids`.
Left null, the host takes no address on any bridge.
'';
};
address = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "10.0.10.2/24";
description = ''
Host static management address in CIDR form.
Left null, the host takes its management address by DHCP.
''; '';
}; };
}; };
};
config = lib.mkIf cfg.enable { config = lib.mkIf (cfg.ids != [ ]) {
assertions = [ networking = {
{ useDHCP = false;
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids; networkmanager.unmanaged = [
message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids."; "interface-name:${cfg.interface}"
}
{
assertion = cfg.management.address == null || cfg.management.vlan != null;
message = "modules.network.vlans.management.address needs modules.network.vlans.management.vlan to say which bridge carries it.";
}
];
# networkd owns these interfaces so NetworkManager does not contend for them.
networking.networkmanager.unmanaged = [
"interface-name:${cfg.trunk}"
] ]
++ map (id: "interface-name:${bridgeName id}") cfg.ids ++ map (id: "interface-name:${util.bridgeName id}") cfg.ids
++ map (id: "interface-name:${vlanName id}") cfg.ids; ++ map (id: "interface-name:vlan${toString id}") cfg.ids;
};
systemd.network = { systemd.network = {
enable = true; enable = true;
netdevs = lib.mkMerge ( netdevs = lib.mkMerge (
map (id: { map (id: {
"40-${bridgeName id}" = { "40-${util.bridgeName id}" = {
netdevConfig = { netdevConfig = {
Name = bridgeName id; Name = util.bridgeName id;
Kind = "bridge"; Kind = "bridge";
}; };
}; };
"40-${vlanName id}" = { "40-vlan${toString id}" = {
netdevConfig = { netdevConfig = {
Name = vlanName id; Name = "vlan${toString id}";
Kind = "vlan"; Kind = "vlan";
}; };
vlanConfig.Id = id; vlanConfig.Id = id;
@@ -104,37 +67,25 @@ in
networks = lib.mkMerge ( networks = lib.mkMerge (
[ [
# The trunk carries only tagged frames and takes no address.
{ {
"30-${cfg.trunk}" = { "10-${cfg.interface}" = {
matchConfig.Name = cfg.trunk; matchConfig.Name = cfg.interface;
networkConfig.LinkLocalAddressing = "no"; networkConfig.DHCP = "yes";
linkConfig.RequiredForOnline = "no"; vlan = map (id: "vlan${toString id}") cfg.ids;
vlan = map vlanName cfg.ids; linkConfig.RequiredForOnline = "routable";
}; };
} }
] ]
++ map (id: { ++ map (id: {
# Enslaved to its bridge, carrying no address itself. "40-vlan${toString id}" = {
"40-${vlanName id}" = { matchConfig.Name = "vlan${toString id}";
matchConfig.Name = vlanName id; networkConfig.Bridge = util.bridgeName id;
networkConfig.Bridge = bridgeName id;
linkConfig.RequiredForOnline = "no"; linkConfig.RequiredForOnline = "no";
}; };
# The management bridge carries the host address. "40-${util.bridgeName id}" = {
# Other bridges provide L2 segments for guests. matchConfig.Name = util.bridgeName id;
"40-${bridgeName id}" = lib.mkMerge [
{
matchConfig.Name = bridgeName id;
linkConfig.RequiredForOnline = "no"; linkConfig.RequiredForOnline = "no";
} };
(lib.mkIf (cfg.management.vlan == id) (
if cfg.management.address == null then
{ networkConfig.DHCP = "yes"; }
else
{ address = [ cfg.management.address ]; }
))
];
}) cfg.ids }) cfg.ids
); );
}; };