{ config, lib, my, ... }: # The host networking foundation: per-VLAN bridges over a tagged trunk. let cfg = config.modules.network.vlans; inherit (my) bridgeName; vlanName = id: "${cfg.trunk}.${toString id}"; in { options.modules.network.vlans = { enable = lib.mkEnableOption "host VLAN bridges over a tagged trunk"; trunk = lib.mkOption { type = lib.types.str; example = "enp1s0"; description = '' Physical interface that carries 802.1Q-tagged traffic for every VLAN. Each declared VLAN attaches to its own bridge. ''; }; ids = lib.mkOption { type = lib.types.listOf (lib.types.ints.between 1 4094); default = [ ]; example = [ 10 20 30 ]; description = '' Tagged VLAN ids to materialize. Each id N creates one bridge named `br-vlanN`. ''; }; management = { 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 { assertions = [ { assertion = cfg.management.vlan == null || lib.elem cfg.management.vlan cfg.ids; message = "modules.network.vlans.management.vlan (${toString cfg.management.vlan}) must name one of modules.network.vlans.ids."; } { 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:${vlanName id}") cfg.ids; 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.ids ); networks = lib.mkMerge ( [ # The trunk carries only tagged frames and takes no address. { "30-${cfg.trunk}" = { matchConfig.Name = cfg.trunk; networkConfig.LinkLocalAddressing = "no"; linkConfig.RequiredForOnline = "no"; vlan = map vlanName cfg.ids; }; } ] ++ 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 bridge carries the host address. # Other bridges provide L2 segments 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.ids ); }; }; }