mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 00:55:01 +03:00
2fab6c3d50
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.
199 lines
5.4 KiB
Go
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
|
|
}
|