Files
3x-ui/internal/amneziawgnet/relay_pool_test.go
T
Aleksey Fedorov 2fab6c3d50 perf(amneziawg): reuse relay buffers and bound the bridge connect time (#7043)
Every TCP connection through a built-in AmneziaWG inbound or outbound
allocated two fresh 32 KiB copy buffers, because io.Copy on a *net.TCPConn
goes through ReadFrom/WriteTo and ignores a caller's buffer. Under a stream
of short connections that is a lot of garbage. The relays now copy with
buffers from a sync.Pool; one side of each pair is always a userspace
netstack connection, so no kernel splice path is given up.

The connect to the local Xray bridge and its SOCKS5 handshake had no time
limit, so a bridge that accepted and never answered pinned the relay's
goroutines and descriptors forever. Both are now bounded at 10 s, and the
deadline is cleared once the handshake succeeds so the relayed connection
keeps its half-close behaviour.
2026-11-13 19:43:03 +01:00

199 lines
5.4 KiB
Go

package amneziawgnet
import (
"errors"
"io"
"net"
"net/netip"
"runtime"
"testing"
"time"
)
// A burst of short connections through AmneziaWG used to allocate a fresh 32 KiB copy buffer
// per direction per connection (gigabytes per hour under churn); the pair must be reused.
func TestPipeBothWaysReusesCopyBuffersAcrossConnections(t *testing.T) {
if raceEnabled {
t.Skip("under -race sync.Pool drops a share of Puts at random")
}
payload := make([]byte, 16*1024)
sink := make([]byte, 16*1024)
ping := make([]byte, len("ping")) // the server goroutine's own buffer
relay := func() {
tunnelClient, tunnelSide := tcpPair(t)
upstreamSide, upstreamServer := tcpPair(t)
done := make(chan struct{})
go func() { pipeBothWays(tunnelSide, upstreamSide); close(done) }()
go func() {
_, _ = io.ReadFull(upstreamServer, ping)
_, _ = upstreamServer.Write(payload)
_ = upstreamServer.Close()
}()
_, _ = tunnelClient.Write([]byte("ping"))
_ = tunnelClient.(*net.TCPConn).CloseWrite()
if _, err := io.ReadFull(tunnelClient, sink); err != nil {
t.Fatalf("reply: %v", err)
}
<-done
}
for range 5 {
relay()
}
const conns = 100
var before, after runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&before)
for range conns {
relay()
}
runtime.ReadMemStats(&after)
perConn := (after.TotalAlloc - before.TotalAlloc) / conns
if perConn >= 32*1024 {
t.Fatalf("each relayed connection allocated %d bytes, want the copy buffers reused (< 32 KiB)", perConn)
}
}
// TestDialUpstreamGivesUpOnSilentBridge: a bridge that accepts but never answers (a hung
// Xray) must fail the CONNECT within its bound instead of pinning the relay.
func TestDialUpstreamGivesUpOnSilentBridge(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0") // never accepted: no greeting reply ever comes
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { _ = ln.Close() })
saved := socks5ConnectTimeout
socks5ConnectTimeout = 150 * time.Millisecond
t.Cleanup(func() { socks5ConnectTimeout = saved })
relay := SocksRelay{Addr: ln.Addr().String(), Password: "x"}
done := make(chan error, 1)
go func() {
conn, err := relay.dialUpstream("peer@example", netip.MustParseAddrPort("192.0.2.1:443"))
if conn != nil {
_ = conn.Close()
}
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Fatal("CONNECT succeeded through a bridge that never answered")
}
case <-time.After(2 * time.Second):
t.Fatal("dialUpstream still blocked 2s past the connect bound: a silent bridge pins the relay")
}
}
// TestDialUpstreamLeavesNoDeadlineAfterHandshake: the connect bound covers the handshake
// only, so writes and reads after it has passed still go through.
func TestDialUpstreamLeavesNoDeadlineAfterHandshake(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
t.Cleanup(func() { _ = ln.Close() })
saved := socks5ConnectTimeout
socks5ConnectTimeout = 150 * time.Millisecond
t.Cleanup(func() { socks5ConnectTimeout = saved })
served := make(chan error, 1)
go func() {
c, err := ln.Accept()
if err != nil {
served <- err
return
}
defer c.Close()
served <- serveSocks5Connect(c, []byte("ping"), []byte("late"))
}()
relay := SocksRelay{Addr: ln.Addr().String(), Password: "x"}
conn, err := relay.dialUpstream("peer@example", netip.MustParseAddrPort("192.0.2.1:443"))
if err != nil {
t.Fatalf("dialUpstream: %v", err)
}
defer conn.Close()
guard := time.AfterFunc(5*time.Second, func() { _ = conn.Close() })
defer guard.Stop()
time.Sleep(3 * socks5ConnectTimeout)
if _, err := conn.Write([]byte("ping")); err != nil {
t.Fatalf("write after the connect bound passed: %v (the handshake deadline leaked onto the relay)", err)
}
got := make([]byte, 4)
if _, err := io.ReadFull(conn, got); err != nil {
t.Fatalf("read after the connect bound passed: %v (the handshake deadline leaked onto the relay)", err)
}
if string(got) != "late" {
t.Fatalf("read %q, want %q", got, "late")
}
if err := <-served; err != nil {
t.Fatalf("fake bridge: %v", err)
}
}
// serveSocks5Connect answers one username/password SOCKS5 CONNECT, reads request, then writes reply.
func serveSocks5Connect(c net.Conn, request, reply []byte) error {
buf := make([]byte, 512)
read := func(n int) ([]byte, error) { _, err := io.ReadFull(c, buf[:n]); return buf[:n], err }
hdr, err := read(2) // VER NMETHODS
if err != nil {
return err
}
if _, err := read(int(hdr[1])); err != nil {
return err
}
if _, err := c.Write([]byte{5, 2}); err != nil { // username/password
return err
}
ver, err := read(2) // VER ULEN
if err != nil {
return err
}
if ver[0] != 1 {
return errors.New("bad auth version")
}
if _, err := read(int(ver[1])); err != nil {
return err
}
plen, err := read(1)
if err != nil {
return err
}
if _, err := read(int(plen[0])); err != nil {
return err
}
if _, err := c.Write([]byte{1, 0}); err != nil {
return err
}
req, err := read(4) // VER CMD RSV ATYP
if err != nil {
return err
}
addrLen := map[byte]int{1: 4, 4: 16}[req[3]]
if req[3] == 3 {
l, err := read(1)
if err != nil {
return err
}
addrLen = int(l[0])
}
if _, err := read(addrLen + 2); err != nil {
return err
}
if _, err := c.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}); err != nil {
return err
}
got, err := read(len(request))
if err != nil {
return err
}
if string(got) != string(request) {
return errors.New("bridge read an unexpected request")
}
_, err = c.Write(reply)
return err
}