mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
5e6ffebdf2
The embedded interface's TCP forwarder admits 65535 connections in flight for all peers together, and a forwarded request holds its slot until the handshake ends or gVisor gives up on it. One client sending bare SYNs from fresh source ports therefore took every slot and left the other clients' connections unanswered. The UDP relay likewise opened a SOCKS5 association and a host UDP socket for every fresh tunnel source port with no per-peer bound. The forwarder now names the peer on the SYN, refuses a source no peer owns and caps one peer at 4096 connections; the UDP relay caps one peer at 1024 sessions and drops the datagrams that would need more.
373 lines
12 KiB
Go
373 lines
12 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// newDeadUDPSession builds a socks5UDPSession over real but already-closed
|
|
// sockets, so pump's receive fails immediately and its teardown runs at once.
|
|
func newDeadUDPSession(t *testing.T) *socks5UDPSession {
|
|
t.Helper()
|
|
peer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
if err != nil {
|
|
t.Fatalf("listen udp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = peer.Close() })
|
|
udpConn, err := net.DialUDP("udp", nil, peer.LocalAddr().(*net.UDPAddr))
|
|
if err != nil {
|
|
t.Fatalf("dial udp: %v", err)
|
|
}
|
|
ctrlLn, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen tcp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = ctrlLn.Close() })
|
|
ctrl, err := net.Dial("tcp", ctrlLn.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("dial tcp: %v", err)
|
|
}
|
|
_ = udpConn.Close()
|
|
return &socks5UDPSession{ctrl: ctrl, udpConn: udpConn}
|
|
}
|
|
|
|
// TestUDPRelayPumpOnlyRetiresItsOwnSession pins the flow that survives a
|
|
// duplicate associate: a losing pump must not evict the published session.
|
|
func TestUDPRelayPumpOnlyRetiresItsOwnSession(t *testing.T) {
|
|
relay := NewUDPRelay(SocksRelay{Addr: "127.0.0.1:1", Password: "x"}, nil)
|
|
src := netip.MustParseAddrPort("10.8.1.5:51820")
|
|
|
|
live := newDeadUDPSession(t)
|
|
superseded := newDeadUDPSession(t)
|
|
relay.sessions[src] = live
|
|
|
|
// Returns as soon as receive fails on the closed socket, so no wait is needed.
|
|
relay.pump(src, superseded)
|
|
|
|
got, ok := relay.sessions[src]
|
|
if !ok {
|
|
t.Fatal("live session was evicted: a retiring pump deleted src's entry regardless of which session held it")
|
|
}
|
|
if got != live {
|
|
t.Fatalf("sessions[%v] = %p, want the live session %p", src, got, live)
|
|
}
|
|
}
|
|
|
|
// TestUDPRelayCloseDropsEverySession keeps Close's contract explicit now that
|
|
// pump's teardown is conditional on still owning the key.
|
|
func TestUDPRelayCloseDropsEverySession(t *testing.T) {
|
|
relay := NewUDPRelay(SocksRelay{Addr: "127.0.0.1:1", Password: "x"}, nil)
|
|
for _, s := range []string{"10.8.1.5:51820", "10.8.1.6:2000"} {
|
|
relay.sessions[netip.MustParseAddrPort(s)] = newDeadUDPSession(t)
|
|
}
|
|
|
|
relay.Close()
|
|
|
|
if n := len(relay.sessions); n != 0 {
|
|
t.Fatalf("Close left %d sessions behind, want 0", n)
|
|
}
|
|
}
|
|
|
|
// tcpPair returns a connected pair of real loopback TCP conns; net.Pipe would
|
|
// not do, since these tests turn on CloseWrite, which it does not implement.
|
|
func tcpPair(t *testing.T) (client, server net.Conn) {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
type accepted struct {
|
|
conn net.Conn
|
|
err error
|
|
}
|
|
ch := make(chan accepted, 1)
|
|
go func() {
|
|
c, err := ln.Accept()
|
|
ch <- accepted{c, err}
|
|
}()
|
|
client, err = net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
got := <-ch
|
|
if got.err != nil {
|
|
t.Fatalf("accept: %v", got.err)
|
|
}
|
|
t.Cleanup(func() { _ = client.Close(); _ = got.conn.Close() })
|
|
return client, got.conn
|
|
}
|
|
|
|
// TestPipeBothWaysDeliversReplyAfterHalfClose is the half-close regression: a
|
|
// client that shuts down its write side must still receive the full response.
|
|
func TestPipeBothWaysDeliversReplyAfterHalfClose(t *testing.T) {
|
|
const request = "GET / HTTP/1.0\r\n\r\n"
|
|
const response = "the reply that arrives only after the request is complete"
|
|
|
|
client, a := tcpPair(t)
|
|
b, server := tcpPair(t)
|
|
go pipeBothWays(a, b)
|
|
|
|
if _, err := client.Write([]byte(request)); err != nil {
|
|
t.Fatalf("client write: %v", err)
|
|
}
|
|
// The half-close the old relay treated as "tear the whole pair down".
|
|
if err := client.(*net.TCPConn).CloseWrite(); err != nil {
|
|
t.Fatalf("client CloseWrite: %v", err)
|
|
}
|
|
|
|
_ = server.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
gotReq, err := io.ReadAll(server)
|
|
if err != nil {
|
|
t.Fatalf("server read: %v", err)
|
|
}
|
|
if string(gotReq) != request {
|
|
t.Fatalf("server got request %q, want %q", gotReq, request)
|
|
}
|
|
|
|
if _, err := server.Write([]byte(response)); err != nil {
|
|
t.Fatalf("server write: %v", err)
|
|
}
|
|
if err := server.(*net.TCPConn).CloseWrite(); err != nil {
|
|
t.Fatalf("server CloseWrite: %v", err)
|
|
}
|
|
|
|
_ = client.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
gotResp, err := io.ReadAll(client)
|
|
if err != nil {
|
|
t.Fatalf("client read: %v", err)
|
|
}
|
|
if string(gotResp) != response {
|
|
t.Fatalf("client got response %q, want %q: the reply was cut off by the half-close", gotResp, response)
|
|
}
|
|
}
|
|
|
|
// TestPipeBothWaysClosesWhenBothSidesFinish keeps the teardown contract: both
|
|
// directions ending must return, not hang on the idle bound.
|
|
func TestPipeBothWaysClosesWhenBothSidesFinish(t *testing.T) {
|
|
client, a := tcpPair(t)
|
|
b, server := tcpPair(t)
|
|
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); pipeBothWays(a, b) }()
|
|
|
|
_ = client.(*net.TCPConn).CloseWrite()
|
|
_, _ = io.ReadAll(server)
|
|
_ = server.(*net.TCPConn).CloseWrite()
|
|
_, _ = io.ReadAll(client)
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
t.Fatal("pipeBothWays did not return after both directions ended")
|
|
}
|
|
}
|
|
|
|
// liveUDPSession returns a session whose udpConn is connected to the returned
|
|
// peer, so a test can hand receive() one exact reply datagram.
|
|
func liveUDPSession(t *testing.T) (*socks5UDPSession, *net.UDPConn) {
|
|
t.Helper()
|
|
peer, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
if err != nil {
|
|
t.Fatalf("listen udp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = peer.Close() })
|
|
udpConn, err := net.DialUDP("udp", nil, peer.LocalAddr().(*net.UDPAddr))
|
|
if err != nil {
|
|
t.Fatalf("dial udp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = udpConn.Close() })
|
|
return &socks5UDPSession{udpConn: udpConn}, peer
|
|
}
|
|
|
|
// sendReply delivers one raw datagram to sess's socket.
|
|
func sendReply(t *testing.T, sess *socks5UDPSession, peer *net.UDPConn, datagram []byte) {
|
|
t.Helper()
|
|
if _, err := peer.WriteToUDP(datagram, sess.udpConn.LocalAddr().(*net.UDPAddr)); err != nil {
|
|
t.Fatalf("write reply: %v", err)
|
|
}
|
|
_ = sess.udpConn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
}
|
|
|
|
// TestSocks5ReceiveDecodesReplyAddressTypes covers all three ATYP forms; the
|
|
// domain form used to misread its own length byte and never skip the name.
|
|
func TestSocks5ReceiveDecodesReplyAddressTypes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
addrPart []byte
|
|
wantAddr string
|
|
}{
|
|
{"IPv4", []byte{0x01, 10, 0, 0, 7}, "10.0.0.7"},
|
|
{"IPv6", append([]byte{0x04}, netip.MustParseAddr("2001:db8::5").AsSlice()...), "2001:db8::5"},
|
|
{"domain holding a literal", append([]byte{0x03, 8}, []byte("10.0.0.9")...), "10.0.0.9"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sess, peer := liveUDPSession(t)
|
|
payload := []byte("the-actual-datagram-payload")
|
|
datagram := append([]byte{0x00, 0x00, 0x00}, tt.addrPart...)
|
|
datagram = append(datagram, 0x1f, 0x90) // port 8080
|
|
datagram = append(datagram, payload...)
|
|
sendReply(t, sess, peer, datagram)
|
|
|
|
buf := make([]byte, 4096)
|
|
from, got, err := sess.receive(buf)
|
|
if err != nil {
|
|
t.Fatalf("receive: %v", err)
|
|
}
|
|
want := netip.AddrPortFrom(netip.MustParseAddr(tt.wantAddr), 8080)
|
|
if from != want {
|
|
t.Errorf("source = %v, want %v", from, want)
|
|
}
|
|
if string(got) != string(payload) {
|
|
t.Errorf("payload = %q, want %q", got, payload)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSocks5ReceiveRejectsTruncatedReplies pins that a short datagram is an
|
|
// error, not a slice-bounds panic in the relay's own pump goroutine.
|
|
func TestSocks5ReceiveRejectsTruncatedReplies(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
datagram []byte
|
|
}{
|
|
{"header only, IPv4 announced", []byte{0x00, 0x00, 0x00, 0x01}},
|
|
{"IPv4 address cut short", []byte{0x00, 0x00, 0x00, 0x01, 10, 0}},
|
|
{"IPv6 address cut short", []byte{0x00, 0x00, 0x00, 0x04, 0x20, 0x01}},
|
|
{"domain length past the end", []byte{0x00, 0x00, 0x00, 0x03, 40, 'a', 'b'}},
|
|
{"address complete but port missing", []byte{0x00, 0x00, 0x00, 0x01, 10, 0, 0, 7}},
|
|
{"unsupported address type", []byte{0x00, 0x00, 0x00, 0x09, 1, 2, 3, 4, 0, 80}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
sess, peer := liveUDPSession(t)
|
|
sendReply(t, sess, peer, tt.datagram)
|
|
buf := make([]byte, 4096)
|
|
if _, _, err := sess.receive(buf); err == nil {
|
|
t.Fatal("receive accepted a malformed datagram instead of returning an error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNewSocks5UDPSessionGivesUpOnSilentServer pins that a control connection
|
|
// the kernel accepts but nobody answers returns within the associate deadline
|
|
// instead of parking Handle -- and with it the tunnel's delivery path -- forever.
|
|
func TestNewSocks5UDPSessionGivesUpOnSilentServer(t *testing.T) {
|
|
// Never accepted: the backlog completes the TCP handshake, the greeting
|
|
// lands in the socket buffer, and no reply ever comes -- a hung Xray.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
saved := socks5AssociateTimeout
|
|
socks5AssociateTimeout = 150 * time.Millisecond
|
|
t.Cleanup(func() { socks5AssociateTimeout = saved })
|
|
|
|
type result struct {
|
|
sess *socks5UDPSession
|
|
err error
|
|
}
|
|
done := make(chan result, 1)
|
|
go func() {
|
|
sess, err := newSocks5UDPSession(ln.Addr().String(), "peer@example", "x")
|
|
done <- result{sess, err}
|
|
}()
|
|
|
|
select {
|
|
case got := <-done:
|
|
if got.err == nil {
|
|
got.sess.Close()
|
|
t.Fatal("associate succeeded against a server that never answered")
|
|
}
|
|
if !errors.Is(got.err, os.ErrDeadlineExceeded) {
|
|
t.Fatalf("associate error = %v, want one wrapping os.ErrDeadlineExceeded", got.err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("newSocks5UDPSession still blocked 2s past the associate deadline: a silent SOCKS5 server parks the tunnel's delivery path")
|
|
}
|
|
}
|
|
|
|
// startFakeSocks5UDP serves just enough no-auth SOCKS5 for UDP ASSOCIATE to
|
|
// succeed, pointing every association at one UDP socket nobody reads.
|
|
func startFakeSocks5UDP(t *testing.T) string {
|
|
t.Helper()
|
|
sink, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
|
if err != nil {
|
|
t.Fatalf("listen udp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = sink.Close() })
|
|
port := sink.LocalAddr().(*net.UDPAddr).Port
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listen tcp: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer c.Close()
|
|
var greeting [4]byte
|
|
var associate [10]byte
|
|
if _, err := io.ReadFull(c, greeting[:]); err != nil {
|
|
return
|
|
}
|
|
_, _ = c.Write([]byte{0x05, 0x00})
|
|
if _, err := io.ReadFull(c, associate[:]); err != nil {
|
|
return
|
|
}
|
|
_, _ = c.Write([]byte{0x05, 0x00, 0x00, 0x01, 127, 0, 0, 1, byte(port >> 8), byte(port)})
|
|
_, _ = io.Copy(io.Discard, c)
|
|
}()
|
|
}
|
|
}()
|
|
return ln.Addr().String()
|
|
}
|
|
|
|
// Each fresh tunnel source port bought a SOCKS5 association and a UDP socket with no
|
|
// per-peer bound, so one client could take the host's UDP ports from everyone.
|
|
func TestUDPRelayCapsSessionsPerPeer(t *testing.T) {
|
|
relay := NewUDPRelay(SocksRelay{Addr: startFakeSocks5UDP(t), Password: "x"}, nil)
|
|
defer relay.Close()
|
|
dst := netip.MustParseAddrPort("192.0.2.1:53")
|
|
|
|
for n := 1; n < maxUDPSessionsPerPeer; n++ {
|
|
relay.slots.acquire("greedy")
|
|
}
|
|
relay.Handle(netip.MustParseAddrPort("10.8.1.5:1000"), dst, "greedy", []byte("x"))
|
|
relay.Handle(netip.MustParseAddrPort("10.8.1.5:1001"), dst, "greedy", []byte("x"))
|
|
relay.Handle(netip.MustParseAddrPort("10.8.1.6:1000"), dst, "modest", []byte("x"))
|
|
|
|
relay.mu.Lock()
|
|
_, last := relay.sessions[netip.MustParseAddrPort("10.8.1.5:1000")]
|
|
_, excess := relay.sessions[netip.MustParseAddrPort("10.8.1.5:1001")]
|
|
_, other := relay.sessions[netip.MustParseAddrPort("10.8.1.6:1000")]
|
|
relay.mu.Unlock()
|
|
if !last || excess || !other {
|
|
t.Fatalf("sessions: greedy's last allowed = %v, greedy's excess = %v, modest's = %v; want true, false, true", last, excess, other)
|
|
}
|
|
|
|
relay.Close()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for !relay.slots.acquire("greedy") {
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("closing greedy's session never released its slot")
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
}
|