router: stagingPort option, move gw-cnx-1 staging uplink into it

The pre-cutover DHCP-client uplink into the old LAN was a hand-written
systemd.network block in the gw-cnx-1 machine config. Make it a router
setting next to trunkPorts/accessPorts so every replacement gateway can
stage the same way, with an assertion that the port is not also the WAN,
a trunk or an access port. gw-cnx-1 sets stagingPort = "enp3s0" in the
inventory; the machine-local block is gone.
This commit is contained in:
2026-09-16 08:30:14 +00:00
parent c0c2193429
commit c9b04711c9
6 changed files with 57 additions and 33 deletions
+1
View File
@@ -32,6 +32,7 @@ inventory.instances.router = {
wan.vlanId = 10; # or null for untagged PPPoE
trunkPorts = [ "enp2s0" ];
accessPorts.enp4s0 = "mgmt"; # untagged on-site recovery port
# stagingPort = "enp3s0"; # DHCP uplink into the old LAN until cutover
vlans = {
mgmt.id = 10;
lan.id = 20;
+16
View File
@@ -250,6 +250,22 @@ in
'';
};
stagingPort = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "enp3s0";
description = ''
Temporary DHCPv4-client uplink into the existing LAN while the box
runs alongside the router it replaces: gives it internet + mesh
before the WAN port is cabled (PPPoE simply retries until then). The
port is in no VLAN zone; the firewall admits only SSH on it. Do NOT
connect the trunk ports to the production switch while staging
Kea on the mgmt tag would fight the old router's DHCP in one
broadcast domain. Set to null at cutover (and usually hand the port
back to `trunkPorts`).
'';
};
vlans = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule vlanModule);
description = "VLANs served at this site; `mgmt` and `lan` are mandatory.";
+19 -1
View File
@@ -21,6 +21,16 @@ in
assertion = lib.all (p: !(cfg.accessPorts ? ${p})) cfg.trunkPorts;
message = "router: a port cannot be both a trunk and an access port.";
}
{
assertion =
cfg.stagingPort == null
|| !(
cfg.stagingPort == cfg.wan.interface
|| lib.elem cfg.stagingPort cfg.trunkPorts
|| cfg.accessPorts ? ${cfg.stagingPort}
);
message = "router: stagingPort ${toString cfg.stagingPort} is also the WAN, a trunk or an access port.";
}
];
# Router diagnostics toolkit: packets (tcpdump), path (mtr), link
@@ -73,7 +83,15 @@ in
let
taggedAll = lib.mapAttrsToList (_: vlan: { VLAN = vlan.id; }) cfg.vlans;
in
{
lib.optionalAttrs (cfg.stagingPort != null) {
# Staging uplink (see interface.nix): plain DHCPv4 client on a spare
# port, no bridge/VLAN membership, so the firewall treats it as untrusted.
"05-staging" = {
matchConfig.Name = cfg.stagingPort;
networkConfig.DHCP = "ipv4";
};
}
// {
# WAN port carries only the PPPoE session; no IP config of its own.
"10-wan" = {
matchConfig.Name = cfg.wan.interface;