Introduce `modules.network`, the host-level networking foundation a Host declares once. A Host states its trunk interface and the tagged VLAN ids to materialize, and the module emits one systemd-networkd bridge per VLAN, named by the `br-vlan<id>` convention, plus the host's own management address on a chosen VLAN's bridge. The trunk and every bridge set `RequiredForOnline = "no"` so wait-online never blocks boot on a carrier-less link, and the module owns its own NetworkManager `unmanaged` guard so enabling it is self-sufficient. Two assertions tie the management VLAN to the declared VLANs and a management address to a VLAN, so an address can never be silently dropped. Enabled on neogaia as a tracer with placeholder values, proving the host build and bridge-name evaluation through `nix flake check`. No Guest is wired to a bridge yet.
146 lines
4.5 KiB
Nix
146 lines
4.5 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
...
|
|
}:
|
|
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
|
let
|
|
cfg = config.modules.network;
|
|
|
|
# Each tagged VLAN materializes as a bridge named for its id.
|
|
bridgeName = id: "br-vlan${toString id}";
|
|
|
|
# The tagged sub-interface stacked on the trunk that feeds one bridge.
|
|
vlanName = id: "${cfg.trunk}.${toString id}";
|
|
in
|
|
{
|
|
options.modules.network = {
|
|
enable =
|
|
lib.mkEnableOption "the host trunk, its tagged-VLAN bridges, and the host management address";
|
|
|
|
trunk = lib.mkOption {
|
|
type = lib.types.str;
|
|
example = "enp1s0";
|
|
description = ''
|
|
The physical interface carrying 802.1Q-tagged traffic for every VLAN.
|
|
Each declared VLAN is stacked on it and enslaved to its own bridge.
|
|
'';
|
|
};
|
|
|
|
vlans = lib.mkOption {
|
|
type = lib.types.listOf (lib.types.ints.between 1 4094);
|
|
default = [ ];
|
|
example = [
|
|
10
|
|
20
|
|
30
|
|
];
|
|
description = ''
|
|
The tagged VLAN ids to materialize. Each id N emits exactly one bridge
|
|
named `br-vlanN`, and guests on VLAN N attach to it.
|
|
'';
|
|
};
|
|
|
|
management = {
|
|
vlan = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.ints.between 1 4094);
|
|
default = null;
|
|
description = ''
|
|
The VLAN whose bridge carries the host's own management address; must
|
|
be one of `vlans`. Left null, the host takes no address on any bridge
|
|
and the bridges serve guests alone.
|
|
'';
|
|
};
|
|
|
|
address = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
example = "10.0.10.2/24";
|
|
description = ''
|
|
The host's static management address, in CIDR form, on the management
|
|
VLAN's bridge. Left null, the host takes its address there by DHCP.
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.vlans;
|
|
message = "modules.network.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.";
|
|
}
|
|
{
|
|
assertion = cfg.management.address == null || cfg.management.vlan != null;
|
|
message = "modules.network.management.address needs modules.network.management.vlan to say which bridge carries it.";
|
|
}
|
|
];
|
|
|
|
# networkd owns the trunk, its VLAN sub-interfaces, and the bridges, so a host
|
|
# also running NetworkManager leaves them to networkd rather than contending.
|
|
networking.networkmanager.unmanaged =
|
|
[ "interface-name:${cfg.trunk}" ]
|
|
++ map (id: "interface-name:${bridgeName id}") cfg.vlans
|
|
++ map (id: "interface-name:${vlanName id}") cfg.vlans;
|
|
|
|
systemd.network = {
|
|
enable = true;
|
|
|
|
netdevs = lib.mkMerge (
|
|
map (id: {
|
|
"40-${bridgeName id}" = {
|
|
netdevConfig = {
|
|
Name = bridgeName id;
|
|
Kind = "bridge";
|
|
};
|
|
};
|
|
"40-${vlanName id}" = {
|
|
netdevConfig = {
|
|
Name = vlanName id;
|
|
Kind = "vlan";
|
|
};
|
|
vlanConfig.Id = id;
|
|
};
|
|
}) cfg.vlans
|
|
);
|
|
|
|
networks = lib.mkMerge (
|
|
[
|
|
# The trunk carries only tagged frames up to the sub-interfaces and
|
|
# takes no address of its own.
|
|
{
|
|
"30-${cfg.trunk}" = {
|
|
matchConfig.Name = cfg.trunk;
|
|
networkConfig.LinkLocalAddressing = "no";
|
|
linkConfig.RequiredForOnline = "no";
|
|
vlan = map vlanName cfg.vlans;
|
|
};
|
|
}
|
|
]
|
|
++ map (id: {
|
|
# Enslaved to its bridge, carrying no address itself.
|
|
"40-${vlanName id}" = {
|
|
matchConfig.Name = vlanName id;
|
|
networkConfig.Bridge = bridgeName id;
|
|
linkConfig.RequiredForOnline = "no";
|
|
};
|
|
# The management VLAN's bridge carries the host address; every other
|
|
# bridge is a plain L2 segment for guests.
|
|
"40-${bridgeName id}" = lib.mkMerge [
|
|
{
|
|
matchConfig.Name = bridgeName id;
|
|
linkConfig.RequiredForOnline = "no";
|
|
}
|
|
(lib.mkIf (cfg.management.vlan == id) (
|
|
if cfg.management.address == null then
|
|
{ networkConfig.DHCP = "yes"; }
|
|
else
|
|
{ address = [ cfg.management.address ]; }
|
|
))
|
|
];
|
|
}) cfg.vlans
|
|
);
|
|
};
|
|
};
|
|
}
|