Skip to content

ci: IPv6-only kind e2e plus the actor veth [DO NOT MERGE — signal only] - #1059

Closed
Yuan Gao (ygao-g) wants to merge 10 commits into
agent-substrate:mainfrom
ygao-g:ci-kind-ipv6-ateomnet
Closed

ci: IPv6-only kind e2e plus the actor veth [DO NOT MERGE — signal only]#1059
Yuan Gao (ygao-g) wants to merge 10 commits into
agent-substrate:mainfrom
ygao-g:ci-kind-ipv6-ateomnet

Conversation

@ygao-g

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

Copy link
Copy Markdown
Collaborator

Not for merging. Same job as #939, one layer further down the stack. #939 shows the IPv6-only e2e failing at TestActorEgress/TestActorEgressHTTPS because an actor has no IPv6 stack at all; this draft adds the actor-veth work on top so we can see what the next failure actually is, rather than predicting it. The run results are the deliverable, not the branch — nothing here should land in this shape.

#1065 supersedes this run with the full stack on a newer base.

Where each commit comes from. This branch is based on the #993 merge, so several are earlier revisions of what their source PR carries today:

commit subject source
09196821 hack: fix DNS on IPv6-only kind clusters #958, earlier revision
e4ca0fcf hack: retry the IPv6-only kind DNS probe instead of asking once #958, since superseded
6d94c78b atenet/router: bind the Envoy ingress listeners dual-stack #911, earlier revision
301bf6f7 atenet/router: bind the admin socket and Service dual-stack #911, earlier revision
f804208a atenet/egress: bind the Envoy sockets and Service dual-stack #911, earlier revision
c91689fa ateomnet: enable IPv6 forwarding in worker pod netns #979 (SURAJ KUMAR (@krsnaSuraj))
3055db5e ateomnet: return nil when sysctl path missing in writeSysctlIfUnset #979 (SURAJ KUMAR (@krsnaSuraj))
1ea34bc4 ateomnet: give the actor veth an IPv6 address and dual-stack nftables rules #1057
aa8e395a ateomnet: skip the actor veth IPv6 setup when the netns has IPv6 disabled #1057
e5703c4f ci: add an IPv6-only kind e2e job #939, earlier revision

#877, #958, #911 and #979 all ride along because GitHub will not take a base branch that lives only in a fork; each drops out as it merges.

A failure here is still the expected outcome, one layer lower. The prediction to check the run against: with the veth dual-stacked, egress should get past network is unreachable and fail instead inside atunnel, which reads the redirected destination with the IPv4-only SO_ORIGINAL_DST. Behind that sits a third layer, dns_lookup_family: V4_ONLY in the egress Envoy config. Locally verified on this exact stack: builds and vets clean for GOOS=linux, and the five root-gated TestSetupActorNetwork* tests pass under sudo in a Linux VM, including the IPv4-only-netns case. The IPv6 CI job itself has never been run locally — ubuntu-latest has no IPv6 egress so the job stands up tayga/NAT64 that a local cluster does not need, which is precisely why this needs to run in CI.

Coverage and gating are unchanged from #939: 2 of 7 suites, gVisor only, cert auth only, single node, no CSI, and the job stays out of the e2e-test merge gate so it can never block a PR. Part of #246.

🤖 Generated with Claude Code

Yuan Gao (ygao-g) and others added 10 commits August 19, 2026 08:54
On a fresh IP_FAMILY=ipv6 cluster nothing resolves from inside a pod and
no actor boots: CoreDNS inherits the node's IPv4 resolver, which a
v6-only pod cannot reach. Two Corefile clauses fix it -- a hosts entry
mapping kind-registry to its IPv6 address, and a forward to an IPv6
upstream, overridable with IPV6_DNS_UPSTREAM. IPv4 and dual-stack
clusters are unchanged, and atenet-egress still crashloops on v6-only
for an unrelated Envoy bind bug.

(cherry picked from commit e336c22)
The probe that checks a pod can resolve an external name asked once, right
after the CoreDNS rollout reported complete, and a brand-new cluster is not
ready by then -- it failed the create on two of three fresh clusters here while
the cluster was merely still settling. Retrying inside the pod does not rescue
it, because a pod that fails keeps failing for the next 36 seconds while a
fresh pod ten seconds later resolves on its first try, so the whole probe pod
now runs up to four times. Five consecutive fresh clusters came up clean.

(cherry picked from commit 533811a)
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 on nothing at all for IPv6. Each primary socket now
carries an additional "::" address on the same port.

Ipv4Compat stays false on it: setting it would clear IPV6_V6ONLY and
collide with the primary already bound to that port, and Envoy rejects
the whole listener when an additional address fails to bind, taking
down all ingress rather than the IPv6 half. IPv4-only clusters are
unaffected -- the primary is untouched, and a host without IPv6 simply
has no second socket to bind. First of three commits binding atenet's
gateways dual-stack.

(cherry picked from commit ed822b5)
The Envoy admin socket bound 0.0.0.0, and the atenet-router Service
carried no ipFamilyPolicy -- which the API server defaults to
SingleStack, one IPv4 ClusterIP and nothing else. Between them the
router had no IPv6 address to answer on. The socket now binds "::"
with ipv4_compat, one socket for both families, and the Service asks
for PreferDualStack.

ipv4_compat is load-bearing rather than incidental: dataplane.go
health-checks the admin listener over http://127.0.0.1:9901/ready, so
a bare "::" would report the dataplane component of /statusz
unhealthy. Prefer, not Require, keeps the Service valid on a
single-stack cluster, where it is a no-op; spec.ipFamilies is left
alone because the primary family is immutable and the API server
appends the secondary itself.

(cherry picked from commit d5edac3)
The gateway's admin and :443 sockets bound 0.0.0.0, so on an
IPv6-primary cluster the kubelet probed the pod on its only address
and atenet-egress crashlooped -- Envoy started fine and logged "admin
address: 0.0.0.0:15000" -- while an actor's CONNECT had no v6 path in.
Both sockets now bind "::" with ipv4_compat, and the Service asks for
PreferDualStack so a dual-stack cluster hands out an IPv6 ClusterIP to
reach them on.

ipv4_compat matters on the admin socket in particular: the ext-proc
sidecar's drainer dials 127.0.0.1:15000, and envoydrain.go reads a
refusal there as "Envoy already exited" and skips the drain silently.

(cherry picked from commit d480c6f)
EnableIPv4Forwarding now also writes
/proc/sys/net/ipv6/conf/all/forwarding so actor IPv6 traffic (including
DNS queries) is routed between the actor veth and pod eth0 instead of
being dropped by ip6_forward() on dual-stack / IPv6-only clusters.

Factor the sysctl write into writeSysctlIfUnset preserving the original
read-only remount/restore behavior, and add unit coverage for its fast
paths.

Fixes: agent-substrate#945
(cherry picked from commit 14e98ae1ff4c29c514e924f0a76a2f27f8e9d11b)
IPv6 sysctls are absent on kernels with IPv6 disabled (e.g. some
containers set net.ipv6.conf.* only when IPv6 is enabled). Treat a
missing path as 'nothing to enable' instead of forcing a remount and
failing, matching the documented behavior.

(cherry picked from commit b01a8b4b3c62614841e38fa14dfa8b0ddeb68731)
… rules

Add an fd00:169:254::/126 point-to-point pair alongside the existing IPv4
addresses, with a matching ::/0 default route, and move the actor nftables
table from ip to inet so a single table carries both families. Each payload
match now guards on NFPROTO to stay off the other family's packets, and
teardown lists the inet family too: naming the wrong family there dumps
empty, takes the "already clean" path, and silently leaks the table.

Assign the IPv6 addresses with IFA_F_NODAD instead of writing the accept_dad
sysctl. The ateom container is unprivileged, so containerd mounts /proc/sys
read-only and the write failed with EROFS, taking SetupActorNetwork and every
actor start down with it on both sandbox classes. A root-gated assertion pins
the flag; the existing tests run as real root, where the sysctl is writable
and the bug is invisible.

(cherry picked from commit 96374378f6730e050e270dca74952a4c0816f89d)
…bled

An IPv4-only cluster leaves net.ipv6.conf.all.disable_ipv6=1 in the worker
pod netns, which is the default on IPv4-only GKE, and netlink there rejects
the veth's IPv6 address with EPERM. The assignment sits on the path of every
SetupActorNetwork call site, so actor startup went from working to failing
outright and the actor never left ResumeGoldenActor. Gate the IPv6 address
and default route on a per-link disable_ipv6 read, leaving the interior
IPv4-only on those clusters instead of failing.

A root-gated test covers a netns with IPv6 disabled; reverting the gate
reproduces the EPERM against it.

(cherry picked from commit 209b7ddc501ab6188b98b5f32b22415bfaf60685)
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.

(cherry picked from commit ef02488)
@ygao-g

Copy link
Copy Markdown
Collaborator Author

Superseded by #1065, which runs the same job on a newer base with the full stack — including #753, #1080 and #1060, which this branch predates. The run there is green; the results here are the older, partial picture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/network DO NOT MERGE This PR must not be merged yet.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants