Files
Bot ef93c2970d networking: add a tunnel mode that follows the host's route
nat and bridged both stop working when a VPN owns the host's default route. That is not a bug in either: vmnet masquerades out of a physical interface, so a guest behind a WireGuard or WARP tunnel is pinned to an interface the tunnel does not use, and its traffic leaves unencrypted or not at all. Nothing about the host's configuration can fix that.

This adds a third mode. The guest's NIC is carried by this process: frames arrive over a socket pair from a VZFileHandleNetworkDeviceAttachment, a small in-process network answers DHCP, ARP and ICMP and terminates the guest's TCP, and UDP is forwarded per flow. Egress leaves through ordinary host sockets, so it follows the host's routing table and therefore the tunnel, with no root, no interface, and no change to the host's networking.

The cost is that this is a TCP/IP stack. It has to be honest about what it implements, and the parts it gets wrong are not visible from a call that returns:

- The attachment socket must be non-blocking. The drain loop runs until recv reports EAGAIN on the same serial queue everything else shares, so on a blocking socket it parks in recv the moment the guest goes quiet -- which is exactly when the guest is waiting for a reply. The symptom was DNS queries leaving and no answers coming back.
- start() runs on that queue and calls into the forwarders it owns. Any of those hopping back onto it with sync deadlocks the queue against itself.
- The guest's receive window has to be honoured, or a download stalls halfway with both ends waiting.
- Window scaling has to be negotiated. Without option 3 in our SYN-ACK a guest must keep its window under 64 KiB however much buffer it has, and that ceiling over the round trip is all the connection can carry -- enough for pages, not for video.
- Unacknowledged data has to be sent again, and only the oldest segment of it: resending the whole unacknowledged range spends the connection's bandwidth on copies whenever the peer is merely slow to acknowledge, which is the common case.
- Segments must be cut to the guest's MSS, and a reply that still does not fit fragmented rather than dropped. QUIC rides at the MTU boundary, so an oversized datagram is discarded once per packet and looks like loss.

The tests are split so each level covers what it can. Frame-level tests drive the responder directly and need no VM, no socket and no privilege -- which is why they passed while every one of the bugs above sat in the code. They could not have caught any of the six that only appear under a real event loop, so those now have tests of their own: a socket pair, the DispatchSourceRead, the shared queue and both forwarders, with the guest's end reachable through an initialiser that adopts an existing pair rather than making one.

Egress is verified against a real guest: pages, downloads and video stream over the tunnel while the host's default route belongs to a VPN.

Known limit, not addressed here: in a guest VM the virtual GPU has no VP9/AV1 decode, so YouTube's web player never starts even though ordinary H.264 video (and the same page over nat) does. That is a property of the virtualised device, not of this mode.
2026-10-01 02:46:44 +08:00
..