Skip to content

add optional IPv6 egress via NAT66 (NETWORK_IPV6=nat) - #12

Merged
pigmonkey merged 4 commits into
pigmonkey:masterfrom
brett:feat/optional-ipv6
Jul 24, 2026
Merged

add optional IPv6 egress via NAT66 (NETWORK_IPV6=nat)#12
pigmonkey merged 4 commits into
pigmonkey:masterfrom
brett:feat/optional-ipv6

Conversation

@brett

@brett brett commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
  • Adds opt-in IPv6 egress: set NETWORK_IPV6=nat in a Migrantfile to give the VM a ULA that the host masquerades to its own IPv6 uplink, mirroring the existing IPv4 NAT.
  • Off by default: without NETWORK_IPV6=nat the VM's IPv6 is still dropped at the FORWARD chain, so nothing changes for existing VMs.
  • Network isolation still applies over IPv6: with NETWORK_ISOLATION on (the default), the VM reaches the internet but not the host or LAN.
  • Refused alongside WireGuard: fwmark policy routing is IPv4-only, so IPv6 would bypass the tunnel — the combination is rejected at startup.
  • Permits ICMPv6 on virbr-migrant so neighbor discovery to the ULA gateway works; without it NAT66 reply traffic dies on host firewalls that accept ICMPv6 only from fe80::/10.
  • migrant ip -6 and migrant status surface the VM's IPv6 (ULA) address, read best-effort from the DHCPv6 lease.
  • Existing installs must recreate the migrant network to gain the IPv6 subnet — documented in the README with a migration walkthrough.

Add an opt-in IPv6 path for VMs. By default a migrant VM still has no
routable IPv6 (dropped at the FORWARD chain); setting NETWORK_IPV6=nat in
a Migrantfile gives the guest a ULA and masquerades it to the host's IPv6
uplink, mirroring the IPv4 NAT. Network isolation still blocks the host
and LAN over IPv6.

- migrant ip -6 and migrant status surface the VM's IPv6 (ULA) address
- refused alongside WireGuard (fwmark routing is IPv4-only, so IPv6 would
  bypass the tunnel)
- NETWORK_IPV6 is validated to nat/off; sample Migrantfiles set off
@brett brett changed the title Optional IPv6 egress via NAT66 (NETWORK_IPV6=nat) add optional IPv6 egress via NAT66 (NETWORK_IPV6=nat) Jul 10, 2026
@pigmonkey

Copy link
Copy Markdown
Owner

Probably needs a test VM and a netcheck.py run to verify that this doesn't open up any unexpected holes.

NETWORK_IPV6=nat mirrored the IPv4 RFC1918 rejects in FORWARD but left
guest->host over IPv6 open: the host's on-link ULA is locally delivered,
so it hits INPUT, not FORWARD. Every host IPv6 service was reachable from
the guest, and the blanket ICMPv6 accept answered echo too — both looser
than the IPv4 path.

- add a per-VM ip6tables INPUT reject (CHAIN6), after LIBVIRT_INP so
  DHCPv6/DNS to dnsmasq still work
- settle ICMPv6 at the top of INPUT: accept only NDP (133-136) and PMTUD
  error types (1-4), reject the rest (echo included), scoped per-tap
- reference-count the shared MASQUERADE and forwarding sysctl so they are
  reverted on the last NAT66 VM instead of persisting after teardown
- apply NAT66 masquerade even with NETWORK_ISOLATION=false (was skipped by
  the early-return guard)

netcheck.py gains --ipv6-nat/--ipv6-host-port and NAT66-aware IPv6 tests
(egress must work; host must stay unreachable over TCP and ICMPv6). New
ipv6/ VM boots with NETWORK_IPV6=nat and runs the checks from post-up.
Verified live: egress works, host blocked, teardown reverts cleanly.
@brett

brett commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Added a test VM (ipv6/, NETWORK_IPV6=nat) that runs netcheck.py from its post-up hook. It did surface holes — all now fixed:

  • guest→host over IPv6 was reachable — the host's on-link ULA is locally delivered, so it hit INPUT and skipped the FORWARD rejects. Added a per-VM ip6tables INPUT reject mirroring the IPv4 chain (after LIBVIRT_INP, so DHCPv6/DNS still work).
  • ICMPv6 to the host was wide open — tightened to NDP + PMTUD error types; echo and the rest now rejected.
  • teardown left the MASQUERADE + forwarding sysctl behind — now reference-counted, reverted on the last NAT66 VM.

Verified on a live VM: IPv6 egress works, host unreachable over both TCP and ICMPv6, IPv4 isolation unchanged, teardown reverts cleanly.

@pigmonkey

Copy link
Copy Markdown
Owner

Why is the test VM in the project root instead of with the other test VMs in test/?

… configs

The NAT66 verification VM lived in the project root as ipv6/, next to the
example VMs (arch/, ubuntu/, debian/), but it is a network-isolation test in
the same mold as test/isolation-only/, test/lan-host/, etc. — self-contained
dir, netcheck.py staged by a playbook, host-side bait listener, verify-and-exit.
Move it to test/ipv6-nat/ and make it a true sibling of the other test configs:

- cloud-init: use the shared test/cloud-init.yml (python3 + uv) instead of a
  copy of arch/'s agent cloud-init
- Migrantfile: drop the unused workspace shared folder and loop-image config;
  trim to the terse form the other test configs use; VM_NAME -> test-ipv6-nat
- playbook: drop the now-redundant uv install (cloud-init supplies it); reduce
  to the one-task netcheck staging the siblings use
- hooks: drop the logging-only post-down for parity (pre-up/post-up/pre-down)
- .gitignore: make the workspace/, wireguard.conf, and .listener*.pid patterns
  depth-agnostic (**/) so they cover configs nested under test/ — also closes a
  latent gap for the existing tcp/udp-host-port listener pidfiles
- docs: update test/README.md and the CLAUDE.md example-VM note accordingly

Verified end-to-end on KVM: netcheck passes — NAT66 egress works and the host
stays unreachable over IPv6.
@brett

brett commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Good point.

I moved the NAT66 verification VM out of the project root and into test/ipv6-nat/ (174d206) — it's a network-isolation test in the same mold as test/isolation-only/, test/lan-host/, etc., so it belongs with them rather than beside the example VMs.

While relocating I also converged it onto the test-config conventions: shared test/cloud-init.yml (dropping the copy of arch's agent cloud-init and the unused workspace share), a one-task netcheck playbook, and the standard pre-up/post-up/pre-down hook set.

Verified end-to-end on KVM — netcheck passes (NAT66 egress works; host stays unreachable over IPv6).

@pigmonkey

Copy link
Copy Markdown
Owner

What is the value of describing the test/ipv6-nat/ VM in the CLAUDE.md file? The other test VMs are not mentioned there.

@brett

brett commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

it was leftover from before that directory moved under test -- removed.

@pigmonkey
pigmonkey merged commit 91c7636 into pigmonkey:master Jul 24, 2026
@brett
brett deleted the feat/optional-ipv6 branch July 27, 2026 04:08
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.

2 participants