Skip to content

ci: add an IPv6-only kind e2e job [DO NOT MERGE — signal only] - #939

Draft
Yuan Gao (ygao-g) wants to merge 6 commits into
agent-substrate:mainfrom
ygao-g:ci-kind-ipv6
Draft

ci: add an IPv6-only kind e2e job [DO NOT MERGE — signal only]#939
Yuan Gao (ygao-g) wants to merge 6 commits into
agent-substrate:mainfrom
ygao-g:ci-kind-ipv6

Conversation

@ygao-g

@ygao-g Yuan Gao (ygao-g) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Not for merging in its current state. This draft exists to produce IPv6-only e2e signal on demand — for current HEAD plus the pending IPv6 stack — because no such coverage exists anywhere in CI today. It carries its dependencies in the diff and runs on every PR; neither is a shape that should land. The run results are the deliverable here, not the branch.

The job builds a single-stack v6 kind cluster with NAT64 + DNS64 on the runner, does a full --deploy-ate-system install, and runs the demo and networking suites. It asserts the cluster really is single-stack before testing anything, and guards against a vacuously green run.

It is red, and that is the point. At this HEAD TestActorEgress fails: an actor has no IPv6 stack, because the sandbox netns gets a hardcoded IPv4 link-local /30 with v4-only masquerade (internal/ateomnet/net.go). Issue to follow. The other failure, TestDurableDirLifecycle, is a pre-existing cert-minting flake — the same demo_test.go:543 PermissionDenied fires on the IPv4 job. Results: demo 17 pass / 5 skip, networking 3 pass / 1 fail. Coverage is deliberately narrower than the IPv4 job — 2 of 7 suites, gVisor only, cert auth only, single node, no CSI.

Stacked on #877 (the IP_FAMILY knob), #958 (DNS on v6-only clusters), and #911 (all three Envoy/Service dual-stack commits) — GitHub will not take a base branch that lives only in a fork, so those ride along and drop out as each merges. Once they have all landed this reduces to a single workflow file with a real trigger — a ci/ipv6 label gate (see the TODO(246) in the header) or a paths: filter — and that is the version worth reviewing for merge. Could a maintainer create the ci/ipv6 label in the meantime? The job stays out of the e2e-test gate regardless, so it never blocks a PR. Part of #246.

@ygao-g
Yuan Gao (ygao-g) force-pushed the ci-kind-ipv6 branch 4 times, most recently from 2d6ebff to 72285e4 Compare August 14, 2026 18:04
IP_FAMILY selects ipv4, ipv6 or dual and becomes networking.ipFamily, leaving
kind's per-family subnet defaults alone. The script also recreates a pre-IPv6
"kind" Docker network, fails fast if the daemon has IPv6 off, sets proxy_ndp
alongside proxy_arp for gVisor pod-to-pod traffic, and repoints an ipv6
kubeconfig from [::1] at localhost so a client outside the Docker host can
still reach the apiserver.

Tested on kind with all three families: node InternalIPs, Service ClusterIPs
and pod IPs land in the requested families, pod-to-pod and CoreDNS work on
them, and pods still pull through the local registry.
CoreDNS runs dnsPolicy: Default and inherits the node's Docker-generated
/etc/resolv.conf, which always names an IPv4 resolver -- unreachable from a
v6-only pod, so every external lookup dies at "connect: network is
unreachable". Behind that sits a second failure: this script wires the
registry into containerd on the node, but atelet pulls actor images from its
own pod netns, where kind-registry does not resolve at all.

Rewrite the Corefile on IPv6-only clusters: a hosts block mapping
kind-registry to the registry's GlobalIPv6Address, with fallthrough so
non-registry names still reach the new IPv6 forwarder. Hard-fail if the
substitution was a no-op, and probe both names from a pod rather than the
node, which is dual-stack and would resolve them either way.
The HTTP and HTTPS ingress listeners bound 0.0.0.0 only, so on a
dual-stack cluster Envoy answered on the router Service's IPv4 ClusterIP
and nothing at all on its IPv6 one. Pair each primary socket with an
additional "::" address on the same port.

Ipv4Compat is false on the additional address. Setting it would clear
IPV6_V6ONLY and collide with the primary IPv4 wildcard already bound to
that port, and Envoy rejects the whole listener when an additional
address fails to bind -- that would take down all ingress, not just the
IPv6 half. Hoisting the literal into a helper keeps the two listeners
from drifting.

No behaviour change on an IPv4-only cluster: the primary address is
untouched, and a host without IPv6 simply has no second socket to bind.
The Envoy admin socket bound 0.0.0.0, leaving it reachable over IPv4 only.
It binds "::" with ipv4_compat now -- one socket for both families.
ipv4_compat is required rather than incidental here: Envoy sets
IPV6_V6ONLY without it, and dataplane.go health-checks the listener over
http://127.0.0.1:9901/ready, so dropping it would take the dataplane
component of /statusz unhealthy.

The atenet-router Service carried no ipFamilyPolicy, which the API server
defaults to SingleStack -- an IPv4 ClusterIP and nothing else, which
leaves the listeners above with no IPv6 address to answer on. Prefer, not
Require, so this stays valid on a single-stack cluster, where it is a
no-op. spec.ipFamilies is deliberately left alone: the primary family is
immutable and the API server appends the secondary one itself.
…families

The gateway's two Envoy sockets bound 0.0.0.0, so on an IPv6-primary
cluster the kubelet's startup probe against the admin port was refused and
atenet-egress crashlooped while Envoy itself started fine and logged "admin
address: 0.0.0.0:15000". The :443 listener had the same gap, leaving no v6
path for an actor's CONNECT.

Both are single sockets, so they bind "::" with ipv4_compat rather than
taking the additional-address pairing the ingress listeners use. On the
admin socket ipv4_compat is load-bearing: the ext-proc sidecar's drainer
reaches it at 127.0.0.1:15000 and envoydrain.go reads a refusal as "Envoy
already exited", so a bare "::" would silently skip the drain. The Service
gets PreferDualStack for the same reason the router's does -- without it a
dual-stack cluster hands out one ClusterIP and the new v6 bind is
unreachable.
Runs the full install plus the demo and networking e2e suites against a
single-stack IPv6-only kind cluster, and asserts the cluster really is
v6-only so a green run cannot quietly become a second IPv4 run. It stays
out of the e2e-test merge gate, so it reports IPv6 status without being
able to block a PR, and it runs on every PR for now so the results are
visible; the TODO on the trigger records the intended ci/ipv6 label gate.

ubuntu-latest has no IPv6 egress, so the job stands up tayga for NAT64
and points CoreDNS at an upstream resolver through the well-known
prefix. DNS64 is scoped to a catch-all server block: synthesizing AAAA
over the cluster zones destroys the v6-only ClusterIP answers and the
control plane never comes up.
@ygao-g Yuan Gao (ygao-g) changed the title ci: add a label-gated IPv6-only kind e2e job ci: add an IPv6-only kind e2e job [DO NOT MERGE — signal only] Aug 14, 2026
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