Skip to content

fix(networking): enforce Windows container egress at the Hyper-V firewall - #140

Merged
luthermonson merged 1 commit into
mainfrom
fix/windows-hyperv-egress
Aug 8, 2026
Merged

fix(networking): enforce Windows container egress at the Hyper-V firewall#140
luthermonson merged 1 commit into
mainfrom
fix/windows-hyperv-egress

Conversation

@luthermonson

Copy link
Copy Markdown
Contributor

Problem

#136 tried to block container→LAN egress on Windows with host Windows Defender Firewall rules (netsh advfirewall), scoped localip=<container subnet 10.88.0.0/16> on the theory that MPSSVC would see the container's source IP on the WinNAT-forwarded flow.

On real hardware it does nothing. Windows Firewall (WFP/MPSSVC) evaluates forwarded container→LAN traffic post-NAT at the host endpoint, where the source is the host's own LAN address — so a rule scoped to the 10.88/16 container source matches nothing. #136's own PR body flagged this assumption as unverified-without-hardware. Now verified false: the containment suite (.github/workflows/containment.yml, Windows "Fleet management planes must be unreachable") ran on a live v0.1.6 node (where #136's installFirewallRules() executes at startup) and still reached:

FAIL: reached Proxmox coyotes at 192.168.5.1:8006
FAIL: reached Proxmox kings  at 192.168.5.2:8006
FAIL: reached Incus daemon   at 192.168.12.113:8443
FAIL: reached Grafana        at 192.168.10.45:3000

The host firewall is simply the wrong enforcement point for NATed container egress.

Fix: New-NetFirewallHyperVRule

The node is Windows Server 2025 (build 26100), which ships the Hyper-V firewall cmdlets. The Hyper-V firewall filters at the container's vNIC boundary, BEFORE NAT, where the packet still carries the container's own address — the correct enforcement point for Hyper-V-isolated Windows containers, and Microsoft's purpose-built mechanism for exactly this.

  • VMCreatorId targeting. Rules are scoped by -VMCreatorId so they apply to container ports, not the host or unrelated Hyper-V workloads. There is no publicly documented fixed GUID for the Windows container creator (only WSL's {40E0AC32-46A5-438A-A0B2-2B479E8F2E90} is documented), and HNS assigns it per host — so rather than hardcode an unverifiable constant, the creator is discovered at runtime via Get-NetFirewallHyperVVMCreator, excluding the well-known WSL creator. (Regular Hyper-V VMs aren't filtered by the Hyper-V firewall at all — it governs container-class workloads — so this can't touch real VMs.)
  • Direction/Action. Outbound Block rules for RFC1918 + link-local (10/8, 172.16/12, 192.168/16, 169.254/16, matching egressBlockedCIDRs and the Linux posture).
  • Address ranges. -RemoteAddresses accepts CIDRs and start-end ranges as a string[], rendered as a real PowerShell array.

What's allow-listed (and why)

  • The gateway (10.88.0.1) — DNS, NAT, default route, module-proxy GatewayPorts. Blocking it bricks all container networking.
  • Container-to-container (10.88.0.0/16).
  • The entire internet — GitHub, Docker Hub, registries. Only RFC1918 + link-local is denied; the creator's default outbound action stays Allow.

Rather than rely on Hyper-V-firewall rule-priority ordering (allow-above-deny) to rescue the gateway — an unverified semantic, and unverified assumptions are exactly what sank #136 — the container subnet (which contains the gateway) is subtracted from every blocked range up front (subtractCIDR), so the gateway never appears inside a Block rule in the first place. 10/8 becomes 10.0.0.0-10.87.255.255, 10.89.0.0-10.255.255.255. The narrow container→gateway control-plane blocks (containerd / dispatch gRPC / debug exec, one TCP port each) mirror the Linux INPUT drops and leave DNS (53) and NAT intact.

Idempotency, cleanup, graceful degradation

  • Idempotent: remove-before-add per rule.
  • Named with the findable ephemerd-egress- prefix; removed on Cleanup (by prefix, catching stale rules from a crashed run).
  • Graceful: if the Hyper-V firewall cmdlets are absent (older Windows) or no container creator is registered yet, it falls back to the retained netsh rules; any individual failure logs a WARN and continues — it never fails daemon startup, degrading to the per-endpoint HNS ACLs.

Tests

Table-style unit tests assert the rule spec (VMCreatorId scoping, ranges, Outbound/Block, control ports, the gateway/subnet never blocked) and the exact PowerShell rendering — without executing anything. go build ./..., GOOS=windows go vet, GOOS=linux go vet, and go test ./pkg/networking/... all pass.

Not verified here

I could not run this against real hardware. The container VMCreatorId resolves at runtime, the Set-NetFirewallHyperVVMSetting enable step, and actual enforcement all need a live run on the Server 2025 node. The containment suite's Windows step is the regression gate — it currently FAILs and must go green once this deploys.

…wall

#136 blocked container->LAN egress with host Windows Defender Firewall
rules (netsh advfirewall) scoped to the container subnet as source. On
real hardware this is a no-op: the host firewall (WFP/MPSSVC) evaluates
NATed container egress post-NAT, where the source is the host's own LAN
address, so a rule scoped to 10.88/16 matches nothing. Confirmed against
a live v0.1.6 Windows node -- the containment suite still reached
Proxmox, Incus, and Grafana.

Replace it with Hyper-V firewall rules (New-NetFirewallHyperVRule),
which filter at the container vNIC boundary BEFORE NAT, where the packet
still carries the container's own address. Rules are scoped by
-VMCreatorId (discovered at runtime via Get-NetFirewallHyperVVMCreator,
excluding the well-known WSL creator) so they apply to job containers,
not the host or unrelated Hyper-V workloads.

The gateway (DNS/NAT/default route) and the container-to-container range
are carved out of every blocked range up front (subtractCIDR), so they
never appear in a Block rule -- no reliance on rule-priority ordering,
which is the kind of unverified firewall semantic that sank #136. The
internet, including GitHub/Docker Hub/registries, is untouched.

Idempotent (remove-before-add), findable/removable by the ephemerd-
name prefix, and graceful: hosts without the Hyper-V firewall cmdlets
fall back to the netsh rules, and any failure warns instead of failing
daemon startup. netsh construction is retained as that fallback.

Needs a live run on the Server 2025 node to confirm; the containment
suite's Windows step is the regression gate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant