From 6436fd9c5ce35567702bf114b4c324c03a5c7822 Mon Sep 17 00:00:00 2001 From: Kuzz007 Date: Tue, 4 Aug 2026 02:05:32 +0300 Subject: [PATCH] Fix download/upload throughput asymmetry in embedded AmneziaWG stackTun's incomingPacket channel (the handoff from gVisor's internal sender to amneziawg-go's TUN-reading/encrypt/UDP-send goroutine) was unbuffered. WriteNotify pushes into it synchronously from whatever gVisor goroutine is sending TCP data, so every single outbound (download/server->client) packet forced a full blocking round trip between gVisor and the single RoutineReadFromTUN goroutine, one packet at a time with no pipelining. The upload/client->server direction has no equivalent stall: Write -> InjectInbound -> DeliverNetworkPacket hands off into gVisor's own ~1MB per-connection TCP receive buffer and returns immediately. That asymmetry -- not a tunable window size -- is what produced observed download throughput far below upload on a real test connection. Give incomingPacket the same queue depth as the channel endpoint's own outbound queue (1024) so gVisor can get meaningfully ahead of the encrypt/send loop instead of blocking on every packet. --- internal/amneziawgnet/netstack.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/amneziawgnet/netstack.go b/internal/amneziawgnet/netstack.go index 984ebceaa..6dd0f3ace 100644 --- a/internal/amneziawgnet/netstack.go +++ b/internal/amneziawgnet/netstack.go @@ -30,6 +30,11 @@ import ( "gvisor.dev/gvisor/pkg/tcpip/transport/udp" ) +// tunQueueDepth is the outbound packet queue depth for both the gVisor +// channel endpoint and the handoff channel to amneziawg-go's TUN reader +// (see the stackTun literal in createNetTUNWithStack for why both need it). +const tunQueueDepth = 1024 + // stackTun implements amneziawg-go's tun.Device directly against a gVisor // channel endpoint, the same approach amneziawg-go's own tun/netstack // package and xray-core's proxy/wireguard/netstack.go both take. Neither of @@ -61,10 +66,24 @@ func createNetTUNWithStack(localAddresses []netip.Addr, mtu int) (awgtun.Device, HandleLocal: false, } dev := &stackTun{ - ep: channel.New(1024, uint32(mtu), ""), + // tunQueueDepth matches channel.New's own outbound queue depth + // below. WriteNotify (called synchronously from whatever gVisor + // goroutine is sending TCP data for the download/server->client + // direction) pushes into incomingPacket; RoutineReadFromTUN (a + // single amneziawg-go goroutine that encrypts and sends each + // packet over UDP) is the only reader. With no buffer, every + // outbound packet forced a full synchronous handoff between the + // two -- gVisor's sender blocked until the encrypt loop was ready + // for the next one, one packet at a time, no pipelining. The + // upload/client->server direction has no equivalent stall: + // Write->InjectInbound->DeliverNetworkPacket hands off into + // gVisor's own ~1MB per-connection TCP receive buffer and returns + // immediately. Buffering this channel gives the download + // direction the same slack the upload direction already had. + ep: channel.New(tunQueueDepth, uint32(mtu), ""), stack: stack.New(opts), events: make(chan awgtun.Event, 10), - incomingPacket: make(chan *buffer.View), + incomingPacket: make(chan *buffer.View, tunQueueDepth), mtu: mtu, } sackEnabledOpt := tcpip.TCPSACKEnabled(true)