Skip to content

ateomnet: give the actor an IPv6 address when the pod has one - #1057

Draft
Yuan Gao (ygao-g) wants to merge 2 commits into
agent-substrate:mainfrom
ygao-g:ateomnet-ipv6-dualstack
Draft

ateomnet: give the actor an IPv6 address when the pod has one#1057
Yuan Gao (ygao-g) wants to merge 2 commits into
agent-substrate:mainfrom
ygao-g:ateomnet-ipv6-dualstack

Conversation

@ygao-g

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

Copy link
Copy Markdown
Collaborator

Part of #246. Do not merge before #979 and #753.

Actor networking is IPv4-only, so an actor on a dual-stack worker pod cannot
reach an IPv6-only destination at all. SetupActorNetwork now assigns the
fd00:169:254::/126 counterparts of the existing point-to-point pair to both
ends of the actor veth, installs an IPv6 default route in the interior netns,
and adds the matching rules to the inet-family actor table.

Whether the actor gets IPv6 is decided once in the worker pod netns and carried
into the interior one, which is created fresh and so always reports IPv6
available whatever the cluster's families are. Both halves have to hold: the pod
needs a global IPv6 address of its own, and the veth has to accept an IPv6
address — IPv4-only GKE sets disable_ipv6 and netlink then rejects the
assignment with EPERM.

Stacked on #1116

The first commit here is #1116, which moves the actor nftables table from the
ip family to inet with no behaviour change on IPv4. Review it there; GitHub
cannot target a base branch that lives on a fork, so it shows up in this diff
too. Only the second commit is new in this PR.

Dependencies

  • ateomnet: enable IPv6 forwarding in worker pod netns #979 — without net.ipv6.conf.all.forwarding=1 in the pod netns, the new
    default route is a black hole. Measured: 100% packet loss in a netns replica
    at all.forwarding=0, 0% at =1.
  • atunnel: support IPv6 original destination lookup #753atunnel recovers the destination with
    getsockopt(SOL_IP, SO_ORIGINAL_DST) into a 16-byte RawSockaddrInet4, so an
    IPv6 connection REDIRECTed into the tunnel is accepted and then dies with no
    usable CONNECT authority. That function's own TODO asks for the IPv6 variant
    "when actor veth setup gains dual-stack support" — which is this PR.

Even with both, dual-stack egress is red: #1089, caught by the test in #1104.
This lands the address, not a working dual-stack egress path.

Testing

Green CI here means the family gate stays shut on an IPv4 cluster, not that
the IPv6 datapath works.
There is no dual-stack CI lane, so none of the IPv6
behaviour below is exercised there.

TestSetupActorNetworkIPv6Gate is the truth table, root-gated, five cases.
Reverting either half of the gate fails exactly one case and nothing else, and
reverting IFA_F_NODAD fails exactly the two that expect IPv6.

TestActorNftablesRuleExprs pins the IPv6 rule expressions alongside the IPv4
ones, and needs no root. TestSetupActorNetworkInstallsEgressRedirect checks
the kernel takes both redirects in one inet nat chain; the IPv6 rule is
installed whatever the gate decided, and simply matches nothing when the actor
has no IPv6 source address.

Not covered

  • The interior eth0 is not observable under gVisor, so only the netns-level
    setup is asserted.
  • Router → worker is still IPv4.
  • The micro-VM guest is still configured IPv4-only, so a micro-VM actor gets no
    IPv6 even where the host veth has one. There is a TODO(#246) at that call
    site.

🤖 Generated with Claude Code

@ygao-g
Yuan Gao (ygao-g) marked this pull request as ready for review August 20, 2026 21:10
@ygao-g
Yuan Gao (ygao-g) marked this pull request as draft August 20, 2026 21:49
@ygao-g Yuan Gao (ygao-g) changed the title ateomnet: give the actor veth an IPv6 address and dual-stack nftables rules ateomnet: give the actor an IPv6 address when the pod has one Aug 21, 2026
Comment thread cmd/ateom-microvm/run.go
Comment on lines +1089 to +1091
// TODO(#246): the guest is configured IPv4-only, so a micro-VM actor sees no
// IPv6 even on a dual-stack pod where the host veth has one. gVisor reads the
// interior netns and picks the address up; this path has to be told.

@BenTheElder Benjamin Elder (BenTheElder) Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not merge new gVisor only features. This should be resolved prior to merge.

e2e tests should exercise both in lockstep.

Benjamin Elder (BenTheElder) added a commit to BenTheElder/substrate that referenced this pull request Aug 21, 2026
Both ateoms now build actor networking through internal/actornet, so
ateomnet's version has had no caller outside its own package since the
micro-VM ateom moved. This deletes it: SetupActorNetwork, NetworkConfig,
CleanupActorNetwork, ConfigureActorVeth, the nftables install and remove,
their expression helpers, and PodIPv4 and DumpNetInfo, which only those
used. What stays is what actornet actually consumes -- the veth constants
and addresses, MustParse*, EnableIPv4Forwarding, TCPProtocol, and the
namespace helpers.

Leaving it would not have been merely untidy. It creates a table named
ateom_actor in the ip family, and actornet creates one under the same name
for the actors that are really running; a table name is unique per family,
so the two are invisible to each other's cleanup and either could delete
or shadow the other's. That is latent while nothing calls the dead path,
and stops being latent the moment anyone does.

The table name moves to actornet with it, unexported, since actornet is
now its only creator. The value is unchanged so an ateom rolling onto this
code still recognises and clears the table its predecessor left behind.

Two smaller leftovers of the same replacement: prepareOCIBundles no longer
takes the ateom pod UID, because the namespace it writes into the bundle is
keyed by actor now; and ateomnet's tests go with the code they covered,
after porting the three that pin behaviour actornet still has -- repeatable
setup, the fixed host-veth MAC, and the interior-link sweep. The last two
are micro-VM requirements, so losing their coverage would have been a real
gap rather than a bookkeeping one.

NOTE: this collides with agent-substrate#1116, which moves the same table to the inet
family, and with agent-substrate#1057 behind it. Both are dual-stack work on code this
deletes. Whichever lands first, the other side is a delete-vs-modify
conflict -- loud, and resolved by keeping the delete -- but the dual-stack
change will need to be made in actornet instead. Worth settling before any
of this becomes a real PR.
The actor's NAT and filter rules lived in an ip table, which can only
ever carry IPv4. They are now in an inet table, so one table can hold
both address families when the actor veth becomes dual-stack. Every
IPv4 match opens with an NFPROTO comparison, because a bare payload
match in an inet table would read an IPv4 offset out of an IPv6 header.
IPv4 behaviour is unchanged, but the move is not a no-op on a dual-stack
pod: inet nat chains register the nat hooks for both families, so IPv6
traffic in the worker pod netns is now conntracked, and the forward
accept now covers IPv6. NAT in the inet family needs Linux 5.2 or later.

Teardown sweeps ip as well as inet. A table name is unique per family,
so the ip table an earlier ateom left behind is invisible to an
inet-only cleanup: the dump comes back empty, the "already clean" path
reports success, and the stale table keeps redirecting alongside the
new one.

Part of agent-substrate#246
Actor networking was IPv4-only, so an actor on a dual-stack worker pod
could not reach an IPv6-only destination at all. SetupActorNetwork now
assigns the fd00:169:254::/126 counterparts of the existing point-to-point
pair to both ends of the actor veth, installs an IPv6 default route in the
interior netns, and adds the matching rules to the inet-family actor table.

Whether the actor gets IPv6 is decided once in the worker pod netns and
carried into the interior one, which is created fresh and so always reports
IPv6 available whatever the cluster's families are. Both halves have to
hold: the pod needs a global IPv6 address of its own, and the veth has to
accept an IPv6 address -- IPv4-only GKE sets disable_ipv6 and netlink then
rejects the assignment with EPERM. Addresses carry IFA_F_NODAD rather than
the accept_dad sysctl, which the unprivileged ateom container cannot write.

Part of agent-substrate#246
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