Selaa lähdekoodia

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 with cfd596a4, the missing
PersistentKeepalive with 8f162994, and the manager lock inversion the same
thread flagged with e95fe80f. 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
Sanaei 18 tuntia sitten
vanhempi
sitoutus
cfd4f64a79
2 muutettua tiedostoa jossa 50 lisäystä ja 1 poistoa
  1. 8 1
      internal/amneziawgnet/relay.go
  2. 42 0
      internal/amneziawgnet/relay_test.go

+ 8 - 1
internal/amneziawgnet/relay.go

@@ -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 {

+ 42 - 0
internal/amneziawgnet/relay_test.go

@@ -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")
+	}
+}