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.
This commit is contained in:
Kuzz007
2026-08-04 02:05:32 +03:00
parent 738163699e
commit 6436fd9c5c
+21 -2
View File
@@ -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)