feat(web-arm): IPv6 egress for podman containers via a reusable NAT66/ULA network #85
Labels
No labels
bug
enhancement
in-progress
needs-info
needs-triage
p0
ready-for-agent
ready-for-human
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/nixos#85
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
web-arm now has working host outbound IPv6 (static /64 + default route on enp1s0,
ADR 0010 / #81). Its podman containers do not — they sit on the default podman
bridge, which is IPv4-only, so a container cannot reach a v6-only destination. This
builds the missing container-egress path as reusable, opt-in infrastructure (a new
container that needs a v6-only destination will be deployed onto it later).
Add a dual-stack podman bridge network any container can opt into:
v6egress) with a private IPv4 subnet and a ULA IPv6subnet (
fd00::/8). netavark masquerades both to the host's sources, so the containerreaches the v4 and v6 internet via NAT — the v6 source being the host GUA
2a01:4f8:c012:43b::1.fwprovisions per-workloadpodman networks (
podman network exists … || podman network create …), ordered beforethe containers that attach to it.
net.ipv6.conf.all.forwarding). Compatible with#81's static-address / accept_ra-off setup; does not disturb the host's own v6 route.
--network=v6egress; collabora/rustdesk are left on the defaultv4 network, untouched.
NAT66 from a ULA is chosen over a routed GUA sub-prefix: egress-only (no inbound surface),
no Hetzner-side work, and it doesn't touch the host /64 from #81. Record in a new ADR 0011.
Acceptance criteria
created idempotently, surviving reboot and redeploy
net.ipv6.conf.all.forwardingis enabled on web-arm(
curl -6/ping6to a known global v6 address succeeds)2a01:4f8:c012:43b::1(NAT66 confirmed)docs/adr/0011-*.mdadded (NAT66/ULA over routed-prefix; podman, no backend flip;egress-only; no-rDNS/SPF caveat)
Blocked by
None — #81 (host IPv6) is merged. Can start immediately.
Agent Brief
Category: enhancement
Summary: Give web-arm's podman containers an opt-in IPv6 egress path — a reusable
dual-stack podman bridge network whose ULA v6 subnet netavark masquerades (NAT66) to the
host's global v6 source, so a container can reach v6-only destinations.
Current behavior:
web-arm (Hetzner Cloud CAX / ARM) has working host outbound IPv6 since #81 / ADR 0010 —
static
2a01:4f8:c012:43b::1/64on enp1s0, default route viafe80::1. Its containers rununder podman:
virtualisation.oci-containerssets nobackend, so it resolves to thenixos-25.11 default
podman— NOT docker. (virtualisation.docker.enableis on, but onlyfor sa-core's direct
docker run; the oci-containers units arepodman-*.) The liveoci-containers are
collaboraandrustdesk-server, on the default podman bridge, which isIPv4-only. There is no v6
networking.nat, no v6-enabled podman network, andnet.ipv6.conf.all.forwardingis unset. Net effect: a container cannot reach a v6-only host.Desired behavior:
A new container (deployed later by the maintainer) must reach a v6-only destination. Build a
reusable, opt-in egress path, not a one-off:
ULA IPv6 /64 (
fd00::/8). netavark masquerades both to the host; the v6 egress sourceis the host GUA
2a01:4f8:c012:43b::1.Key interfaces / contracts (NixOS options — durable; ignore current file layout):
systemd.services.<name>oneshot that idempotently ensures the podman network existswith both subnets,
wants/afterpodman.service, orderedbeforethepodman-*.serviceunits that attach to it. Mirror the existing in-repo pattern for provisioning podman
networks (see how host
fwsets up per-workload networks for piped/invidious:podman network exists <net> || podman network create --ipv6 --subnet <v4> --subnet <v6> <net>).boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true. Deployed-host contract:sysctl net.ipv6.conf.all.forwarding= 1, without perturbing #81's static v6 address/route(accept_ra stays off; addressing is static).
virtualisation.oci-containers.containers.<name>withextraOptions = [ "--network=<net>" ]. (Real container comes later; validate with athrowaway one.)
docs/adr/0011-*.md(next free number at implementation time).Literal values:
2a01:4f8:c012:43b::1— already on enp1s0 from #81; do notredeclare it.
fd00::/8/64, e.g.fdaa:bbcc:ddee::/64. NAT'd, so the value is free —just keep it stable and ULA.
bridge (10.88.0.0/16), e.g.
10.89.0.0/24.v6egress.Acceptance criteria:
idempotently, surviving reboot and
nixos-rebuildnet.ipv6.conf.all.forwarding= 1 on the deployed host(
curl -6/ping6to a known global v6 address succeeds)2a01:4f8:c012:43b::1(e.g.curl -6 https://ifconfig.cofrom the container returns the host GUA)collaboraandrustdesk-serverstay on the default network and keep workingcurl -6from host) and outbound mail (IPv4 per ADR 0010 —postconf smtp_address_preference= ipv4) are unchangeddocs/adr/0011-*.mdrecords: NAT66/ULA over routed GUA sub-prefix (which would need anNDP proxy or 2nd Hetzner /64 and would touch #81's on-link /64); podman kept (no backend
flip); egress-only (no inbound surface); and that the NAT66 source GUA has no rDNS/SPF —
any future container sending SMTP over this network inherits the exact deliverability
problem ADR 0010's Postfix v4-pin exists to avoid
Out of scope:
reusable network, validated with a throwaway container.
collabora/rustdesk-server— they don't opt in.virtualisation.docker.enablemismatch and the vestigialrustdesk-server∈dockergroup — pre-existing warts; do not flipoci-containers.backendor refactor them here.