feat(network): add host VLAN-bridge networking foundation #29

Merged
alexion merged 1 commits from task-0003-network-vlan-foundation into main 2026-07-25 16:30:12 -04:00
Owner

Summary

Introduce modules.network, the host-level networking foundation a Host declares once and every Guest will later attach to.
A Host states its trunk interface and the set of tagged VLAN ids to materialize, and the module emits exactly 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.

Each VLAN materializes as a <trunk>.<id> tagged sub-interface stacked on the trunk, a br-vlan<id> bridge, and a network enslaving the sub-interface to the bridge.
The trunk and every bridge set RequiredForOnline = "no" so systemd-networkd-wait-online never blocks boot on a carrier-less link.
The management address is a static CIDR, or DHCP when unset, on the management VLAN's bridge alone.

This slice is the host-level foundation only; no Guest is wired to a bridge yet.

No host enables it yet

The repo's only host, neogaia, is a wifi laptop on an access port. It can't present guests as L2 citizens (wifi doesn't bridge), and enabling the module there turns on systemd-networkd and pulls in systemd-resolved, which takes over the laptop's DNS — an unwanted change to a daily machine. So the module ships inert; the standing enablement lands with the first wired server host.

The foundation is still fully verified, and reproducibly so from this tree:

  • nix flake check — passes (module was enabled on neogaia transiently to confirm the full toplevel builds, then reverted).

  • Bridge names, VLAN tags, trunk stacking, sub-interface→bridge enslavement, and the management address were confirmed by nix eval.

  • Reproducible without touching any host file, by enabling the module ad hoc:

    nix eval --impure --expr '((builtins.getFlake (toString ./.)).nixosConfigurations.neogaia.extendModules {
      modules = [ { modules.network = { enable = true; trunk = "enp1s0"; vlans = [ 10 20 ]; }; } ];
    }).config.systemd.network.netdevs' --apply 'ns: builtins.filter (s: builtins.match "br-vlan.*" s != null) (map (n: n.netdevConfig.Name) (builtins.attrValues ns))'
    # => [ "br-vlan10" "br-vlan20" ]
    

Notable decisions

  • The module owns its own NetworkManager unmanaged guard for the trunk, sub-interfaces, and bridges, so enabling it is self-sufficient on a host that also runs NetworkManager.
  • Two assertions guard placement: the management VLAN must be one of the declared VLANs, and a declared management address must name a management VLAN, so an address can never be silently dropped.
  • A management.gateway option was considered and dropped as speculative for this slice.

Review

Risk

Overall: Low (revised down after removing the host enablement)

  • Blast radius: Low — one new module, inert until a host enables it; no host file changed on merge.
  • Reversibility: Low — a single additive file, trivial revert, no migration or persisted state.
  • Test coverage: Medium — no unit framework; nix flake check builds the toplevel and eval-checks the assertions, and the module was exercised via a transient enablement, but no standing host build covers it.
  • Sensitive domain: Low — the module touches host networking, but it ships enabled on nothing, so it alters no running system.
  • Size & complexity: Low — small, focused, straightforward control flow.
  • Runtime criticality: Low — inert until a future wired host opts in.

Standards and Spec

All Standards and Spec findings raised in review were addressed: comments split to one sentence per line and the file-top header reduced to a single purpose line, the cross-file narration and duplicated comments removed, the speculative gateway option dropped, the silent management-address drop closed with a second assertion, and the NetworkManager guard moved into the module so enabling it is self-sufficient.

— Claude

## Summary Introduce `modules.network`, the host-level networking foundation a Host declares once and every Guest will later attach to. A Host states its trunk interface and the set of tagged VLAN ids to materialize, and the module emits exactly 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. Each VLAN materializes as a `<trunk>.<id>` tagged sub-interface stacked on the trunk, a `br-vlan<id>` bridge, and a network enslaving the sub-interface to the bridge. The trunk and every bridge set `RequiredForOnline = "no"` so `systemd-networkd-wait-online` never blocks boot on a carrier-less link. The management address is a static CIDR, or DHCP when unset, on the management VLAN's bridge alone. This slice is the host-level foundation only; no Guest is wired to a bridge yet. ### No host enables it yet The repo's only host, `neogaia`, is a wifi laptop on an access port. It can't present guests as L2 citizens (wifi doesn't bridge), and enabling the module there turns on `systemd-networkd` **and pulls in `systemd-resolved`, which takes over the laptop's DNS** — an unwanted change to a daily machine. So the module ships inert; the standing enablement lands with the first wired server host. The foundation is still fully verified, and reproducibly so from this tree: - `nix flake check` — passes (module was enabled on `neogaia` transiently to confirm the full toplevel builds, then reverted). - Bridge names, VLAN tags, trunk stacking, sub-interface→bridge enslavement, and the management address were confirmed by `nix eval`. - Reproducible without touching any host file, by enabling the module ad hoc: ``` nix eval --impure --expr '((builtins.getFlake (toString ./.)).nixosConfigurations.neogaia.extendModules { modules = [ { modules.network = { enable = true; trunk = "enp1s0"; vlans = [ 10 20 ]; }; } ]; }).config.systemd.network.netdevs' --apply 'ns: builtins.filter (s: builtins.match "br-vlan.*" s != null) (map (n: n.netdevConfig.Name) (builtins.attrValues ns))' # => [ "br-vlan10" "br-vlan20" ] ``` ### Notable decisions - The module owns its own NetworkManager `unmanaged` guard for the trunk, sub-interfaces, and bridges, so enabling it is self-sufficient on a host that also runs NetworkManager. - Two assertions guard placement: the management VLAN must be one of the declared VLANs, and a declared management address must name a management VLAN, so an address can never be silently dropped. - A `management.gateway` option was considered and dropped as speculative for this slice. ## Review ### Risk **Overall: Low** (revised down after removing the host enablement) - Blast radius: Low — one new module, inert until a host enables it; no host file changed on merge. - Reversibility: Low — a single additive file, trivial revert, no migration or persisted state. - Test coverage: Medium — no unit framework; `nix flake check` builds the toplevel and eval-checks the assertions, and the module was exercised via a transient enablement, but no standing host build covers it. - Sensitive domain: Low — the module touches host networking, but it ships enabled on nothing, so it alters no running system. - Size & complexity: Low — small, focused, straightforward control flow. - Runtime criticality: Low — inert until a future wired host opts in. ### Standards and Spec All Standards and Spec findings raised in review were addressed: comments split to one sentence per line and the file-top header reduced to a single purpose line, the cross-file narration and duplicated comments removed, the speculative `gateway` option dropped, the silent management-address drop closed with a second assertion, and the NetworkManager guard moved into the module so enabling it is self-sufficient. — Claude
alexion added 1 commit 2026-07-25 16:18:18 -04:00
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.

No host commits an enablement: the only host is a wifi laptop that cannot
present guests as L2 citizens and whose DNS the module's networkd/resolved
would disturb, so the standing enablement waits for the first wired server
host. The build and bridge-name evaluation were verified by enabling the
module ad hoc, and stay reproducible through `extendModules`.
alexion force-pushed task-0003-network-vlan-foundation from b87076ebd3 to e05adef7b7 2026-07-25 16:18:18 -04:00 Compare
alexion merged commit e05adef7b7 into main 2026-07-25 16:30:12 -04:00
alexion deleted branch task-0003-network-vlan-foundation 2026-07-25 16:30:12 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alexion/dotfiles#29