mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-16 15:17:14 +00:00
fix(amneziawg): bound the SOCKS5 UDP associate exchange
newSocks5UDPSession only bounded the dial. The greeting, auth and UDP ASSOCIATE reads on the control connection had no deadline, and they run inline in UDPRelay.Handle -- on the peer's receive goroutine that delivers decrypted packets into gVisor. A SOCKS5 server the kernel accepts for but that never answers (a wedged Xray: its listen backlog still completes TCP handshakes) therefore parked that goroutine, and every later packet from the peer behind it, TCP included, for as long as Xray stayed wedged. One deadline now covers the dial plus the whole exchange and is cleared once the association is up, since after that the control connection is only held open. The test drives the exchange against a listener that is never accepted, which is exactly the hung-server shape. This was the last of the three defects confirmed on the issue: the header protection key that could not be cleared went withcfd596a4, the missing PersistentKeepalive with8f162994, and the manager lock inversion the same thread flagged withe95fe80f. The session death the issue was opened for is not a panel defect. The reporter's own capture on the host NIC shows the client's packets stop reaching the VPS after the first burst, the server never sees a second handshake initiation from it, and nothing the panel sends is outside what a stock amneziawg-go 3.1 server sends (the Apple client embeds the same library build). That is a client- or path-side stop, which no server-side change can address. Closes #6323
This commit is contained in:
@@ -143,6 +143,10 @@ type socks5UDPSession struct {
|
||||
udpConn *net.UDPConn
|
||||
}
|
||||
|
||||
// Bounds dial plus the greeting/auth/associate reads: an accepted-but-silent
|
||||
// server otherwise parks Handle, and with it the tunnel's delivery path.
|
||||
var socks5AssociateTimeout = 5 * time.Second
|
||||
|
||||
// newSocks5UDPSession performs the SOCKS5 greeting, username/password auth,
|
||||
// and UDP ASSOCIATE request/reply by hand: golang.org/x/net/proxy's SOCKS5
|
||||
// client (used by RelayTCP above) only implements CONNECT, and xray-core's
|
||||
@@ -150,11 +154,13 @@ type socks5UDPSession struct {
|
||||
// types, not reusable as a standalone dialer -- so this is a small, direct,
|
||||
// from-the-RFC implementation rather than an existing library call.
|
||||
func newSocks5UDPSession(addr, user, password string) (*socks5UDPSession, error) {
|
||||
dialer := net.Dialer{Timeout: 5 * time.Second}
|
||||
deadline := time.Now().Add(socks5AssociateTimeout)
|
||||
dialer := net.Dialer{Deadline: deadline}
|
||||
ctrl, err := dialer.DialContext(context.Background(), "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("amneziawgnet: dial SOCKS5 control connection: %w", err)
|
||||
}
|
||||
_ = ctrl.SetDeadline(deadline)
|
||||
if err := socks5Handshake(ctrl, user, password); err != nil {
|
||||
ctrl.Close()
|
||||
return nil, err
|
||||
@@ -171,6 +177,7 @@ func newSocks5UDPSession(addr, user, password string) (*socks5UDPSession, error)
|
||||
ctrl.Close()
|
||||
return nil, err
|
||||
}
|
||||
_ = ctrl.SetDeadline(time.Time{})
|
||||
|
||||
udpConn, err := net.DialUDP("udp", nil, net.UDPAddrFromAddrPort(bind))
|
||||
if err != nil {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package amneziawgnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -255,3 +257,43 @@ func TestSocks5ReceiveRejectsTruncatedReplies(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user