Repository navigation
Merged
Conversation
The guest's IP packets leave over VXLAN on an ordinary UDP socket to an exit that is plain Linux configuration; sandboxd keeps identity, policy and the source rewrite. Frames, virtio-net headers and IPv4 headers are bounds checked and every refusal is counted by reason; a partial checksum must sit in the L4 checksum field, which closes a fuzz-found bypass where a guest aimed the checksum write at the IPv4 destination after the policy check. A GSO frame is segmented with full checksums straight into the send buffer and charged for every packet it puts on the wire; a packet or segment over the MTU is dropped. Per-claim limits and flows outlive a stream reconnect. VXLAN and ARP are accepted only from the configured exits. Inbound TCP is merged into GSO frames after checksum verification, and ICMP errors quote the guest address. Overlay addresses come from an in-memory local allocator with an append-only release log, or from one statement per claim against a cell's PostgreSQL table.
…rics
A pool with "tun": true gives each claim an overlay address before its claims
commit and a packet door on vsock port 2051 next to its proxy doors; tun_up is
sent in the background, so the claim path adds no RPC. The address, the switch
and the exit label follow the claim through hibernate, archive, checkpoint,
fork and move, and are released with it. PUT /v1/sandboxes/{id}/tun (root
only) switches a claim; a disable runs beside any transition and the hello
re-checks the switch under the manager lock, while an enable or a label change
waits for the transition. A resend rewrites an archive row a failed write
missed. The quota counter is persisted at disarm, at the move commit, on any
claim write and at a clean stop. Ended flows write tun_flow audit records and,
with egress_usage_bytes, egress_bytes usage events. CI runs the kernel exit
test as root and each fuzz target for 30 seconds.
Each arm (claim, wake, move-in, restart) starts with an empty flow table, and inbound traffic passed only for flows the claim had opened on this node. A receive-only connection that crossed a move, a wake or a restart then never sent the outbound packet that would reopen its flow: the server's retransmissions were dropped and the stream hung (seen on test1/test2 after a live move with a download open). An inbound TCP segment that is not a bare SYN now reopens its flow when the claim's policy allows the peer, under the claim's flow limits; a refused one leaves no state behind. Unsolicited inbound is still stopped by the exit's NAT conntrack, and the guest resets a non-SYN segment to a closed port.
x/net's ReadBatch allocated a *net.UDPAddr for every datagram, and the GCs that churn caused emptied the frame-buffer pool, so a busy node re-made 64 KiB buffers. Linux now reads with recvmmsg into headers, addresses and buffers the receiver owns, with the poller callback bound once; the receive callback and the vsock writer's buffer list no longer escape per batch. An alloc profile of the kernel-exit test shows no allocation left on either direction's loop.
- A tun_up still in flight after a re-arm sent tun_down once its door was replaced, leaving the guest down behind a live door; it now does so only when the claim has no door at all. - A stream keeps the MTU its hello announced, so a reload that lowers tun.mtu no longer drops live guests' full-size packets. - A clean stop ends every live flow, so flows that outlive the process are recorded (audit and egress_bytes) up to the stop. - A guest of a tun pool that ends up without a door (disabled while asleep, no address, the pool's tun turned off) is sent tun_down in the background; a door found disabled after it was installed is taken down again. - A GSO frame of more than 128 packets is dropped as size. - Inbound bytes are counted without the virtio-net header. - Inbound TCP merges only for a guest that offered CSUM and TSO4, and only segments with the same window. - A guest's ICMP error quotes the claim address, so the exit relates it. - The packet door survives an accept error; a guest that stops reading for 10 s has its stream closed. - The tun fields are read under the manager lock as one snapshot. - The local allocator writes its release log outside the lock Assign takes. - Usage events name the destination ip:port.
…s in the Go SDK make go-test runs every benchmark once. The benchmark link had no MTU since streams carry their own, so every frame dropped as size. The sandbox summary also serves tun_addr, tun_off and tun_profile, which the SDK's SandboxSummary now carries; the checkpoint record's switch and label and the pool's tun flag stay host-side, like no_egress and egress, and join the parity test's skips.
… counts - A profile or enable change commits only onto the state it planned from; a disable that overtook it makes it re-plan, so the stale plan can no longer turn a disabled claim back on. - Checkpoint and promote read the switch and label through the manager lock. - A packet's bytes are counted on its flow under the flow table's read lock, so an idle or disarm sweep never records a flow before that count. - A claim that holds an address but wakes into a pool whose tun is off is sent tun_down. - The API doc says when a running claim's replica row picks the change up.
CMGS
added a commit
that referenced
this pull request
Nov 6, 2026
…refuses tun The list lived in three copies (the API refusal, the seed hash, reload's diff) and #478 added tun to only one: PUT /v1/pools answered 200 for "tun": true and dropped it, and a tun edit made every restart warn that the pools differ.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The host side of the advanced network mode (#471). Stacked on #474 (guest side: silkd
tun0, the packet protocol on vsock 2051); merge #474 first. The proxy path stays the default network mode.What it does
A pool with
"tun": truegives each claim an overlay address and a packet door on vsock port 2051 next to its proxy doors. sandboxd carries the guest's IP packets over VXLAN, on an ordinary UDP socket, to an exit: plain Linux configuration (a learning VXLAN device, forwarding, NAT) on the same host or on a gateway machine. sandboxd needs no TUN, no routes and no NAT rules on the node.EvalFlow(barehost: "*"rules) and the internal-address guard, which also refuses the overlay subnet and100.64.0.0/30. Per-claim byte, packet and new-flow rates, a flow cap and a quota apply before any further work; a GSO frame is charged for every packet and header it puts on the wire.tun-freed.logfor the reuse window; a mesh requiresmeta_store.celland allocates with one statement per claim in a cell table, failing closed while the meta store is down. Addresses follow the claim through hibernate, archive, checkpoint, fork and live move.PUT /v1/sandboxes/{id}/tun(root only) disables or re-enables a claim and sets its exit label, including on hibernated and archived claims. A disable revokes the packet path at once and runs beside any transition; an enable or a label change waits for it.tun_upis sent in the background after arm, so the claim path adds no RPC; a guest of a tun pool that ends up without a door is senttun_down. A TCP stream that predates an arm (move, wake, restart) resumes: a non-SYN segment reopens its flow under the claim's policy and limits.tun_flowaudit record; withegress_usage_byteseach allowed flow appends anegress_bytesusage event (IP bytes,ip:portreference,upstreamtun/<label>). A clean stop ends live flows so they are recorded up to the stop./metricsadds frame, datagram, ARP and per-reason drop counters.docs/advanced-network.mdhost section with a single-host exit reference config and a configuration table;deploy.md,sandboxd-api.md,egress.md.Fuzz-found checksum-offset policy bypass
VIRTIO_NET_HDR_F_NEEDS_CSUMset, the incremental source rewrite patches the 16-bit word atcsum_start + csum_offset. A guest could aim that offset into the IPv4 header, so the rewrite edited the destination address after the policy check had judged the original one: the packet left for a destination the policy never saw. A mismatched GSO type (TCP GSO on a UDP packet) also panicked the segmenter.Parserefuses a frame whose header does not fit its packet: the GSO type must match the protocol, and a partial checksum must start at the L4 header (csum_start20) with the offset of that protocol's checksum field (16 for TCP, 6 for UDP); ICMP may not ask for one. Such frames drop asgso.FuzzParseAndRewriteasserts that after the rewrite the source is the claim address and the destination is unchanged; its corpus seed for the bypass (testdata/fuzz/FuzzParseAndRewrite/a7fe022f…) and the segmenter panic seed (FuzzSegment/3560257e…) run on everygo test;TestParseReadsPortsAndRefusesWhatV1DoesNotCarrycovers the refused headers. CI runs each of the four fuzz targets for 30 s.Configuration
The VXLAN port is a config item:
tun.port(default 4789) is the exits' port, andtun.listenthe node's socket. The CI kernel-exit test uses 4789 inside its own namespaces; the hardware kit used 4790 because the testbed already runs another VXLAN on 4789. Full table indocs/advanced-network.md#configuration; restart-only fields are refused on reload.Hot-path cost
tun: one map lookup.tun_upruns in the background.go test -bench . ./tunnet/, arm64 laptop, 0 allocs/op each): a 63 KB GSO frame segmented with full checksums and VXLAN headers straight into the send buffer 8.8–9.0 µs (about 200 ns per wire packet); a small packet 104 ns; an inbound packet delivered 251–261 ns.recvmmsginto owned buffers replacedx/netReadBatch, which allocated an address per datagram).strace -con sandboxd and the frame/datagram counters):sendmsgwith UDP GSO per frame (about 27 datagrams per call)sendtoper guest ACK framerecvmmsg, 4.3 ACKs per callrecvmmsg, about 30 datagrams per callwritevper ~6 framesHardware
Two bare-metal testbed hosts (call them test1 and test2), a sandboxd on each in one mesh, PostgreSQL meta store, an exit in its own network namespace on each host. A claim on test2 leaves either through the test2-local exit or through the test1 exit (the "remote" exit below).
Functional (all pass). Rows marked † ran in the first hardware round, on this branch before the review fixes; the others ran again on the review-fixed build.
tun†tun0meta_store.cell†policy), exit forward counter 0tun0removed, PUT 9–20 ms, traffic stops; re-enable restores it within 1 ssenderdrops (VXLAN from a non-exit) during all of the aboveThroughput, interleaved arms in one run (order tun, proxy, proxy, tun), local exit, 4 GiB each way
recvmmsg), tun download was 1.49 Gbit/s and UDP 64-byte delivery 107k pps; it is now 2.10–2.20 and 320–330k pps.sendmsg(106 µs per 64 KB frame locally, 36 µs to the remote exit). Through the remote exit, upload costs sandboxd 1.9 s/GiB (about 0.6 core at 2.6 Gbit/s).recvmmsgreturns about 15 datagrams per call there instead of 30, so fewer segments merge (5.2 per guest frame) and the guest ACKs every ~10 KB. These are tracked as follow-ups (below), not changed here.Coverage
pool/tun_test.go), hardware for claim, release, hibernate/wake, disable/enableTestTrafficCrossesAKernelExit(a real Linux exit in its own namespaces: learning VXLAN,arp_accept, masquerade, forward and input drops, run as root in CI),TestRouterAnnouncesRewritesAndPassesOnlyRepliesandTestRouterAnswersARPOnlyForArmedClaims(gratuitous ARP to the exit picked by label, ARP answered only for armed claims and only from exits),TestRouterResumesAStreamOpenedBeforeTheArmButRefusesAnOpener(a stream continues on a new arm), the pool move/adopt tests that carry address, switch, label and quota in the claim row. What none of these exercise: an exit's FDB moving between two nodes that are both remote to it, under real traffic.Review
A full-file correctness and flow review, a second flow review on the final diff, a style review and a security review ran on this branch; every finding was checked against the source. Applied, each with a regression test that fails when its fix is reverted:
sizegso_sizemultiplied wire traffic past the rate limits and the quota; frames are charged per wire packettun_upgoroutine senttun_downafter a re-armegress_bytesusage eventtun_upand an fsync of the whole release map ran on the claim path; the PG allocator took three round tripsNot adopted, with reasons:
/8(16M addresses) this is bounded for years of churn, and a collector needs an owner column and a cross-node sweep. Kept as a known gap.Codex review, two rounds on one thread:
tunwas turned off kept a staletun0.e7854b7c."Known gaps and follow-ups
sendtoon download (batching small frames intosendmmsg), and fewer merged segments through a remote exit. Tracked in tun: host packet loop throughput leads after #478 #479 with the profiles above.tun0. Switching redroid totun0is cocoonbox's change.Gates
GOWORK=off make go-test(every module, race plus one pass of every benchmark),make go-lint(5 modules × linux/darwin, 10×0 issues),asl ./...on both GOOS,go test -race ./...on macOS and in a Linux container, the kernel-exit test as root in a Linux container, 30 s per fuzz target, the PostgreSQL tests against a real server.