From 39935ed9ef29b375184113983580052c6d970e97 Mon Sep 17 00:00:00 2001 From: YoungReckless4 <132759713+YoungReckless4@users.noreply.github.com> Date: Thu, 22 Oct 2026 14:23:13 +0300 Subject: [PATCH] fix(tests): stabilize local network fixtures Keep the TUIC stream fragmentation check on a working UDP loopback path, allocate the AmneziaWG fixture ports dynamically, and wait for a real Xray stats response before starting API mutation assertions. Recreate failed readiness channels so an early connection cannot stall the probe. Verified host and offline parallel Go suites, repeated occupied-port and UDP tests, race/shuffle runs, and golangci-lint. Production behavior and dependency versions are unchanged. Split out of #6877 so that pull request's squash carries only the feature. --- internal/amneziawgnet/manager_test.go | 4 +-- internal/tuic/server_test.go | 2 +- internal/tuic/socks_bridge_test.go | 43 ++++++++++++++++++++++----- internal/xray/api_e2e_test.go | 41 +++++++++++++++++++++++++ internal/xray/api_users_e2e_test.go | 1 + 5 files changed, 80 insertions(+), 11 deletions(-) diff --git a/internal/amneziawgnet/manager_test.go b/internal/amneziawgnet/manager_test.go index f067852f2..6baaeb751 100644 --- a/internal/amneziawgnet/manager_test.go +++ b/internal/amneziawgnet/manager_test.go @@ -286,12 +286,12 @@ func TestForwardedPortsOnlyChangeStillReconcilesPortForwards(t *testing.T) { t.Fatalf("generate peer keypair: %v", err) } - const forwardedPort = 58930 + forwardedPort := freePort(t) m := &Manager{ifaces: map[int]*managed{}} inst := amneziawg.Instance{ Id: 6, InterfaceName: "awgtest6", - ListenPort: 58716, + ListenPort: 0, PrivateKey: priv, PublicKey: pub, Address: []string{"10.205.0.1/24"}, diff --git a/internal/tuic/server_test.go b/internal/tuic/server_test.go index 774d60d22..b55f7d19c 100644 --- a/internal/tuic/server_test.go +++ b/internal/tuic/server_test.go @@ -410,7 +410,7 @@ func TestServerUDPStreamE2E(t *testing.T) { func testServerUDPStreamE2E(t *testing.T, controller string) { certPEM, keyPEM := generateTestCert(t) - socksAddr, socksCleanup := startMockSocks5Server(t, "stream@example.com", "mock-socks-pass") + socksAddr, socksCleanup := startMockSocks5ServerWithUDPLoopback(t, udpEchoLoopback(t), "stream@example.com", "mock-socks-pass") defer socksCleanup() testUUID := uuid.New() diff --git a/internal/tuic/socks_bridge_test.go b/internal/tuic/socks_bridge_test.go index e404a5b14..d1e6e5a41 100644 --- a/internal/tuic/socks_bridge_test.go +++ b/internal/tuic/socks_bridge_test.go @@ -179,7 +179,24 @@ func TestPipeBiDirectional(t *testing.T) { } } +func udpEchoLoopback(t *testing.T) string { + t.Helper() + conn, err := net.ListenPacket("udp6", "[::1]:0") + if err == nil { + _ = conn.Close() + return "::1" + } + t.Logf("IPv6 loopback unavailable for SOCKS UDP echo: %v; using IPv4 loopback", err) + return "127.0.0.1" +} + func startMockSocks5Server(t *testing.T, expectedUser, expectedPass string) (string, func()) { + t.Helper() + return startMockSocks5ServerWithUDPLoopback(t, "127.0.0.1", expectedUser, expectedPass) +} + +func startMockSocks5ServerWithUDPLoopback(t *testing.T, udpIP, expectedUser, expectedPass string) (string, func()) { + t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("failed to listen: %v", err) @@ -198,7 +215,7 @@ func startMockSocks5Server(t *testing.T, expectedUser, expectedPass string) (str return } } - go handleMockSocksConn(conn, expectedUser, expectedPass) + go handleMockSocksConnWithUDPLoopback(conn, udpIP, expectedUser, expectedPass) } }() @@ -209,6 +226,10 @@ func startMockSocks5Server(t *testing.T, expectedUser, expectedPass string) (str } func handleMockSocksConn(conn net.Conn, expectedUser, expectedPass string) { + handleMockSocksConnWithUDPLoopback(conn, "127.0.0.1", expectedUser, expectedPass) +} + +func handleMockSocksConnWithUDPLoopback(conn net.Conn, udpIP, expectedUser, expectedPass string) { defer conn.Close() // Read greeting var greeting [4]byte @@ -277,7 +298,7 @@ func handleMockSocksConn(conn net.Conn, expectedUser, expectedPass string) { _, _ = io.Copy(conn, conn) case 0x03: // UDP ASSOCIATE // Bind a UDP listener for the mock - u, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}) + u, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP(udpIP), Port: 0}) if err != nil { return } @@ -285,13 +306,19 @@ func handleMockSocksConn(conn net.Conn, expectedUser, expectedPass string) { bindAddr := u.LocalAddr().(*net.UDPAddr) bindPort := uint16(bindAddr.Port) - resp := make([]byte, 10) + var resp []byte + if bindAddr.IP.To4() != nil { + resp = make([]byte, 10) + resp[3] = 0x01 + copy(resp[4:8], bindAddr.IP.To4()) + binary.BigEndian.PutUint16(resp[8:10], bindPort) + } else { + resp = make([]byte, 22) + resp[3] = 0x04 + copy(resp[4:20], bindAddr.IP.To16()) + binary.BigEndian.PutUint16(resp[20:22], bindPort) + } resp[0] = 0x05 - resp[1] = 0x00 - resp[2] = 0x00 - resp[3] = 0x01 - copy(resp[4:8], bindAddr.IP.To4()) - binary.BigEndian.PutUint16(resp[8:10], bindPort) if _, err := conn.Write(resp); err != nil { return } diff --git a/internal/xray/api_e2e_test.go b/internal/xray/api_e2e_test.go index 0d01eebf9..ed9281645 100644 --- a/internal/xray/api_e2e_test.go +++ b/internal/xray/api_e2e_test.go @@ -1,6 +1,7 @@ package xray import ( + "context" "encoding/json" "fmt" "net" @@ -9,6 +10,8 @@ import ( "path/filepath" "testing" "time" + + statsService "github.com/xtls/xray-core/app/stats/command" ) // TestXrayAPI_E2E exercises the gRPC hot-apply surface (outbounds, inbounds, @@ -87,6 +90,7 @@ func TestXrayAPI_E2E(t *testing.T) { t.Fatalf("api init: %v", err) } defer api.Close() + waitForXrayAPIReady(t, &api, apiPort) // --- outbounds --- socksOutbound := []byte(`{"protocol":"socks","settings":{"servers":[{"address":"127.0.0.1","port":10808}]},"tag":"test-out"}`) @@ -255,3 +259,40 @@ func waitForPort(t *testing.T, port int) { } t.Fatalf("xray api port %d did not open in time", port) } + +func waitForXrayAPIReady(t *testing.T, api *XrayAPI, apiPort int) { + t.Helper() + deadline := time.NewTimer(15 * time.Second) + defer deadline.Stop() + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + var lastErr error + for { + if api.StatsServiceClient == nil { + if err := api.Init(apiPort); err != nil { + t.Fatalf("reinitialize Xray API client: %v", err) + } + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + resp, err := (*api.StatsServiceClient).GetSysStats(ctx, &statsService.SysStatsRequest{}) + cancel() + if err == nil && resp != nil { + return + } + if err != nil { + lastErr = err + } else { + lastErr = fmt.Errorf("GetSysStats returned a nil response") + } + + api.Close() + + select { + case <-deadline.C: + t.Fatalf("xray API did not become ready: %v", lastErr) + case <-ticker.C: + } + } +} diff --git a/internal/xray/api_users_e2e_test.go b/internal/xray/api_users_e2e_test.go index 1631fd3d1..a0881daad 100644 --- a/internal/xray/api_users_e2e_test.go +++ b/internal/xray/api_users_e2e_test.go @@ -111,6 +111,7 @@ func startE2ECore(t *testing.T, inbounds []any) *e2eCore { t.Fatalf("api init: %v", err) } t.Cleanup(api.Close) + waitForXrayAPIReady(t, api, apiPort) return &e2eCore{api: api, cmd: cmd, port: apiPort} }