Generate host-side microVM networking on nexus #4

Merged
vnprc merged 1 commit from agent/microvm-host-network into master 2026-07-29 13:52:22 +01:00
Collaborator

Generates the host-side networking the microVM guests will attach to: the microvm0 bridge, the m-* TAP attachment policy, the manager split with NetworkManager, and egress for 192.168.123.0/24.

This module imports nothing from microvm.nix, composes no host module, and declares no guest, which is why it is implementable now: allod/vm and allod/archetypes carry zero microvm references today, and this slice needs none of them.

Refs vnprc/notes#73

What changes on nexus

The starting state was verified against the composed configuration, not assumed: systemd.network.enable was false, systemd.network.wait-online.enable was true, NetworkManager owned every link, services.resolved.enable was false, and /etc/qemu/bridge.conf was allow virbr0.

networkd runs on this host for the first time. It matches microvm0 and m-* and nothing else, so every other link stays unmatched and therefore NetworkManager's. systemd.network.wait-online.enable is set to false because NetworkManager is responsible for this host's internet connection — upstream's own documentation names exactly that case — and leaving the default would risk a boot-time wait, and a timeout, on an interface set networkd was never meant to own.

Egress is networking.nat with internalIPs = [ "192.168.123.0/24" ] and no externalInterface, which emits exactly iptables -w -t nat -A nixos-nat-post -s '192.168.123.0/24' -j MASQUERADE. Source-based on purpose: binding it to the uplink interface name would need a live-host fact and would break when NetworkManager renames or replaces that interface. IPv4 forwarding is not re-declared; net.ipv4.ip_forward is already true in the composed configuration.

No firewall input is opened on the bridge. That is expressed as absence rather than as an empty per-interface block, because an "empty" networking.firewall.interfaces.<name> block still generates interface-named rules. The deploy-side check asserts the absence.

There is no DHCP server, reservation, lease file, or resolver on this network. Static inventory ownership plus TAP teardown is the whole lifecycle, which is why nothing here has a virsh net-update, dhcp_release, or virbr0.status counterpart. The libvirt path keeps all of those, untouched.

The network values live in one file, modules/microvm-host-network-values.nix, exported as lib.microvmHostNetwork. The nexus module and the microvm-network check in vnprc/deploy both derive from it, so neither carries a second copy of the bridge name or the prefix (architecture.md principle 8).

A host-wide default this turns on, decided explicitly

Turning networkd on flips one default nobody asked for. nixpkgs' networkd module sets services.resolved.enable = mkDefault true, and on this host that would replace NetworkManager's resolv.conf with the resolved stub, add systemd-resolved.service and /etc/systemd/resolved.conf, and remove resolvconf.service — a second resolver stack on a live desktop hypervisor, for a feature nothing in this arc needs. This is the same shape as the hardware.ksm.enable finding the dev plan records against the public host module, and it gets the same treatment: the value is decided explicitly in the private layer (services.resolved.enable = false), and the deploy-side check asserts the chosen value so a later upstream default change is a build failure rather than a silent DNS change on a live host.

With that pinned, the full generated delta against the pre-change nexus configuration is exactly:

  • added services: systemd-networkd, systemd-networkd-wait-online, systemd-networkd-wait-online@ (the latter two generated with enable = false, so no unit file is installed)
  • added socket: systemd-networkd
  • added environment.etc: systemd/network/40-microvm0.netdev, systemd/network/40-microvm0.network, systemd/network/41-microvm-tap.network, systemd/networkd.conf
  • removed: nothing

network-setup.service, resolvconf.service, /etc/resolv.conf handling, and every libvirt artifact are unchanged.

Risk

R3. This changes generated host networking on a live desktop hypervisor that also runs every libvirt domain, so a bad composition disturbs more than the first guest. Three specific residuals for human scrutiny:

Running a second network manager alongside NetworkManager is the substantive change. The mitigation is that networkd matches only two link names, both unmanaged in NetworkManager, and wait-online is off so nothing gates boot. Every .network is required by the check to pin [Match] Name and to use no other match key, because a .network with an empty [Match] claims every link on the host.

The NAT module raises net.ipv4.conf.all.forwarding to true at mkOverride 99. On this host that knob is the same kernel setting as net.ipv4.ip_forward, which is already true, so the runtime effect is nil and the declaration merely becomes coherent — but it is a declared change and is called out rather than buried.

The masquerade rule has no output-interface binding, so it applies to traffic from the microVM prefix leaving any interface. Traffic that stays on microvm0 is bridged rather than routed and does not traverse the nat POSTROUTING hook, so this is the intended source-based shape. It is stated here because the alternative reading — an ! -o microvm0 clause — has no networking.nat option and would have to be hand-written into the firewall script, which is more fragile and less checkable.

Not in this PR, and deliberately: no microvm.nix import, no host module composition, no guest configuration, no per-machine microVM addresses, no ramfs, no volumes, no launcher. Those wait on the plan's Agent Gate — allod/strategy#22 landing at its canonical master path, and allod/nexus#21, #22, #23 landing.

Nothing here has been activated. Host build, activation, and any live network change remain human-only.

Validation

nix flake check path:<profiles> passes.

nix build --no-link path:<deploy>#nixosConfigurations.nexus.config.system.build.toplevel with this branch overridden in builds successfully. All eight nixosConfigurations still evaluate, and every existing deploy#checks.x86_64-linux.* check passes.

The generated networkd units are exactly three files: a microvm0 bridge netdev; a microvm0 .network carrying Address=192.168.123.1/24, ConfigureWithoutCarrier=true, RequiredForOnline=no, and no DHCP or DNS; and an m-* .network carrying Bridge=microvm0 and RequiredForOnline=no.

The paired deploy#checks.x86_64-linux.microvm-network check (vnprc/deploy PR) asserts all of this against the generated artifacts and carries the sabotage evidence: wait-online re-enabled, the masquerade bound to an uplink name, a firewall input opened on the bridge, a third .network match, a .network with no [Match] Name, a DHCP server on the bridge, a resolver on the bridge, services.resolved left at the networkd default, libvirt disabled, /etc/qemu/bridge.conf widened to allow all, and the TAP match repointed off the bridge — each fails with its own message, and the restored copy re-runs clean.

Generates the host-side networking the microVM guests will attach to: the `microvm0` bridge, the `m-*` TAP attachment policy, the manager split with NetworkManager, and egress for `192.168.123.0/24`. This module imports nothing from microvm.nix, composes no host module, and declares no guest, which is why it is implementable now: `allod/vm` and `allod/archetypes` carry zero `microvm` references today, and this slice needs none of them. Refs vnprc/notes#73 ## What changes on nexus The starting state was verified against the composed configuration, not assumed: `systemd.network.enable` was `false`, `systemd.network.wait-online.enable` was `true`, NetworkManager owned every link, `services.resolved.enable` was `false`, and `/etc/qemu/bridge.conf` was `allow virbr0`. networkd runs on this host for the first time. It matches `microvm0` and `m-*` and nothing else, so every other link stays unmatched and therefore NetworkManager's. `systemd.network.wait-online.enable` is set to `false` because NetworkManager is responsible for this host's internet connection — upstream's own documentation names exactly that case — and leaving the default would risk a boot-time wait, and a timeout, on an interface set networkd was never meant to own. Egress is `networking.nat` with `internalIPs = [ "192.168.123.0/24" ]` and no `externalInterface`, which emits exactly `iptables -w -t nat -A nixos-nat-post -s '192.168.123.0/24' -j MASQUERADE`. Source-based on purpose: binding it to the uplink interface name would need a live-host fact and would break when NetworkManager renames or replaces that interface. IPv4 forwarding is not re-declared; `net.ipv4.ip_forward` is already `true` in the composed configuration. No firewall input is opened on the bridge. That is expressed as absence rather than as an empty per-interface block, because an "empty" `networking.firewall.interfaces.<name>` block still generates interface-named rules. The deploy-side check asserts the absence. There is no DHCP server, reservation, lease file, or resolver on this network. Static inventory ownership plus TAP teardown is the whole lifecycle, which is why nothing here has a `virsh net-update`, `dhcp_release`, or `virbr0.status` counterpart. The libvirt path keeps all of those, untouched. The network values live in one file, `modules/microvm-host-network-values.nix`, exported as `lib.microvmHostNetwork`. The nexus module and the `microvm-network` check in `vnprc/deploy` both derive from it, so neither carries a second copy of the bridge name or the prefix (`architecture.md` principle 8). ## A host-wide default this turns on, decided explicitly Turning networkd on flips one default nobody asked for. nixpkgs' networkd module sets `services.resolved.enable = mkDefault true`, and on this host that would replace NetworkManager's `resolv.conf` with the resolved stub, add `systemd-resolved.service` and `/etc/systemd/resolved.conf`, and remove `resolvconf.service` — a second resolver stack on a live desktop hypervisor, for a feature nothing in this arc needs. This is the same shape as the `hardware.ksm.enable` finding the dev plan records against the public host module, and it gets the same treatment: the value is decided explicitly in the private layer (`services.resolved.enable = false`), and the deploy-side check asserts the chosen value so a later upstream default change is a build failure rather than a silent DNS change on a live host. With that pinned, the full generated delta against the pre-change nexus configuration is exactly: - added services: `systemd-networkd`, `systemd-networkd-wait-online`, `systemd-networkd-wait-online@` (the latter two generated with `enable = false`, so no unit file is installed) - added socket: `systemd-networkd` - added `environment.etc`: `systemd/network/40-microvm0.netdev`, `systemd/network/40-microvm0.network`, `systemd/network/41-microvm-tap.network`, `systemd/networkd.conf` - removed: nothing `network-setup.service`, `resolvconf.service`, `/etc/resolv.conf` handling, and every libvirt artifact are unchanged. ## Risk R3. This changes generated host networking on a live desktop hypervisor that also runs every libvirt domain, so a bad composition disturbs more than the first guest. Three specific residuals for human scrutiny: Running a second network manager alongside NetworkManager is the substantive change. The mitigation is that networkd matches only two link names, both unmanaged in NetworkManager, and `wait-online` is off so nothing gates boot. Every `.network` is required by the check to pin `[Match] Name` and to use no other match key, because a `.network` with an empty `[Match]` claims every link on the host. The NAT module raises `net.ipv4.conf.all.forwarding` to `true` at `mkOverride 99`. On this host that knob is the same kernel setting as `net.ipv4.ip_forward`, which is already `true`, so the runtime effect is nil and the declaration merely becomes coherent — but it is a declared change and is called out rather than buried. The masquerade rule has no output-interface binding, so it applies to traffic from the microVM prefix leaving any interface. Traffic that stays on `microvm0` is bridged rather than routed and does not traverse the nat POSTROUTING hook, so this is the intended source-based shape. It is stated here because the alternative reading — an `! -o microvm0` clause — has no `networking.nat` option and would have to be hand-written into the firewall script, which is more fragile and less checkable. Not in this PR, and deliberately: no microvm.nix import, no host module composition, no guest configuration, no per-machine microVM addresses, no ramfs, no volumes, no launcher. Those wait on the plan's Agent Gate — `allod/strategy#22` landing at its canonical `master` path, and `allod/nexus#21`, `#22`, `#23` landing. Nothing here has been activated. Host build, activation, and any live network change remain human-only. ## Validation `nix flake check path:<profiles>` passes. `nix build --no-link path:<deploy>#nixosConfigurations.nexus.config.system.build.toplevel` with this branch overridden in builds successfully. All eight `nixosConfigurations` still evaluate, and every existing `deploy#checks.x86_64-linux.*` check passes. The generated networkd units are exactly three files: a `microvm0` bridge netdev; a `microvm0` `.network` carrying `Address=192.168.123.1/24`, `ConfigureWithoutCarrier=true`, `RequiredForOnline=no`, and no DHCP or DNS; and an `m-*` `.network` carrying `Bridge=microvm0` and `RequiredForOnline=no`. The paired `deploy#checks.x86_64-linux.microvm-network` check (vnprc/deploy PR) asserts all of this against the generated artifacts and carries the sabotage evidence: `wait-online` re-enabled, the masquerade bound to an uplink name, a firewall input opened on the bridge, a third `.network` match, a `.network` with no `[Match] Name`, a DHCP server on the bridge, a resolver on the bridge, `services.resolved` left at the networkd default, libvirt disabled, `/etc/qemu/bridge.conf` widened to `allow all`, and the TAP match repointed off the bridge — each fails with its own message, and the restored copy re-runs clean.
Adds the microvm0 bridge, the m-* TAP attachment policy, the manager split with
NetworkManager, and source-based egress for the microVM prefix. It imports
nothing from microvm.nix and declares no guest, so it composes and is checkable
before any public host module exists.

networkd runs for the first time on this host, matching only microvm0 and m-*;
every other link stays unmatched and therefore NetworkManager's.
systemd.network.wait-online is disabled because NetworkManager owns this host's
internet connection.

Turning networkd on flips one host-wide default nobody asked for: nixpkgs sets
services.resolved.enable to true by default under networkd, which would replace
this host's resolv.conf with the resolved stub and start a second resolver
stack on a live desktop. It is decided explicitly here instead of inherited.

Egress is a source-based masquerade with no uplink interface name, so it needs
no live-host fact and survives a NetworkManager rename of the uplink. No
firewall input is opened on the bridge, and there is no DHCP server, lease
file, or resolver on this network.

Libvirt is untouched: virbr0, 192.168.122.0/24, libvirt's dnsmasq, and
/etc/qemu/bridge.conf all keep their pre-change generated state.

Refs vnprc/notes#73
vnprc approved these changes 2026-07-29 13:52:17 +01:00
vnprc merged commit 0c1f35454b into master 2026-07-29 13:52:22 +01:00
vnprc deleted branch agent/microvm-host-network 2026-07-29 13:52:23 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
vnprc/profiles!4
No description provided.