{ config, lib, pkgs, ... }: with lib; let cfg = config.nix-nos.vlans; vlanType = types.submodule { options = { id = mkOption { type = types.int; description = "VLAN ID (1-4094)"; example = 100; }; interface = mkOption { type = types.str; description = "Parent interface"; example = "eth0"; }; addresses = mkOption { type = types.listOf types.str; default = []; description = "IP addresses for this VLAN interface (CIDR notation)"; example = [ "10.0.100.1/24" ]; }; gateway = mkOption { type = types.nullOr types.str; default = null; description = "Default gateway for this VLAN"; example = "10.0.100.254"; }; dns = mkOption { type = types.listOf types.str; default = []; description = "DNS servers for this VLAN"; example = [ "8.8.8.8" ]; }; mtu = mkOption { type = types.nullOr types.int; default = null; description = "MTU size for this VLAN interface"; example = 1500; }; }; }; in { options.nix-nos.vlans = mkOption { type = types.attrsOf vlanType; default = {}; description = "VLAN configurations using systemd-networkd"; example = literalExpression '' { storage = { id = 100; interface = "eth0"; addresses = [ "10.0.100.1/24" ]; }; mgmt = { id = 200; interface = "eth0"; addresses = [ "10.0.200.1/24" ]; gateway = "10.0.200.254"; }; } ''; }; config = mkIf (cfg != {}) { assertions = [ { assertion = all (name: vlan: vlan.id >= 1 && vlan.id <= 4094 ) (mapAttrsToList nameValuePair cfg); message = "nix-nos.vlans: VLAN ID must be between 1 and 4094"; } { assertion = all (name: vlan: (length vlan.addresses) > 0 ) (mapAttrsToList nameValuePair cfg); message = "nix-nos.vlans: Each VLAN must have at least one address"; } ]; systemd.network.enable = true; # Create VLAN netdevs systemd.network.netdevs = mapAttrs' (name: vlan: nameValuePair "20-${name}" { netdevConfig = { Name = name; Kind = "vlan"; }; vlanConfig.Id = vlan.id; }) cfg; # Configure VLAN networks systemd.network.networks = mkMerge [ # VLAN interface networks (mapAttrs' (name: vlan: nameValuePair "20-${name}" { matchConfig.Name = name; address = vlan.addresses; gateway = optional (vlan.gateway != null) vlan.gateway; dns = vlan.dns; linkConfig = optionalAttrs (vlan.mtu != null) { MTUBytes = toString vlan.mtu; }; }) cfg) # Parent interface VLAN attachment # Group VLANs by parent interface (let vlansByParent = foldl' (acc: nameVlanPair: let name = nameVlanPair.name; vlan = nameVlanPair.value; parent = vlan.interface; in acc // { ${parent} = (acc.${parent} or []) ++ [ name ]; } ) {} (mapAttrsToList nameValuePair cfg); in mapAttrs' (parent: vlanNames: nameValuePair "21-${parent}-vlans" { matchConfig.Name = parent; vlan = vlanNames; }) vlansByParent) ]; }; }