feat(network): add host VLAN-bridge networking foundation
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.
This commit is contained in:
145
modules/network.nix
Normal file
145
modules/network.nix
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
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
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user