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.
This commit is contained in:
YoungReckless4
2026-10-22 14:23:13 +03:00
committed by MHSanaei
parent 96069e9393
commit 39935ed9ef
5 changed files with 80 additions and 11 deletions
+2 -2
View File
@@ -286,12 +286,12 @@ func TestForwardedPortsOnlyChangeStillReconcilesPortForwards(t *testing.T) {
t.Fatalf("generate peer keypair: %v", err) t.Fatalf("generate peer keypair: %v", err)
} }
const forwardedPort = 58930 forwardedPort := freePort(t)
m := &Manager{ifaces: map[int]*managed{}} m := &Manager{ifaces: map[int]*managed{}}
inst := amneziawg.Instance{ inst := amneziawg.Instance{
Id: 6, Id: 6,
InterfaceName: "awgtest6", InterfaceName: "awgtest6",
ListenPort: 58716, ListenPort: 0,
PrivateKey: priv, PrivateKey: priv,
PublicKey: pub, PublicKey: pub,
Address: []string{"10.205.0.1/24"}, Address: []string{"10.205.0.1/24"},
+1 -1
View File
@@ -410,7 +410,7 @@ func TestServerUDPStreamE2E(t *testing.T) {
func testServerUDPStreamE2E(t *testing.T, controller string) { func testServerUDPStreamE2E(t *testing.T, controller string) {
certPEM, keyPEM := generateTestCert(t) 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() defer socksCleanup()
testUUID := uuid.New() testUUID := uuid.New()
+35 -8
View File
@@ -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()) { 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") ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
t.Fatalf("failed to listen: %v", err) t.Fatalf("failed to listen: %v", err)
@@ -198,7 +215,7 @@ func startMockSocks5Server(t *testing.T, expectedUser, expectedPass string) (str
return 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) { 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() defer conn.Close()
// Read greeting // Read greeting
var greeting [4]byte var greeting [4]byte
@@ -277,7 +298,7 @@ func handleMockSocksConn(conn net.Conn, expectedUser, expectedPass string) {
_, _ = io.Copy(conn, conn) _, _ = io.Copy(conn, conn)
case 0x03: // UDP ASSOCIATE case 0x03: // UDP ASSOCIATE
// Bind a UDP listener for the mock // 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 { if err != nil {
return return
} }
@@ -285,13 +306,19 @@ func handleMockSocksConn(conn net.Conn, expectedUser, expectedPass string) {
bindAddr := u.LocalAddr().(*net.UDPAddr) bindAddr := u.LocalAddr().(*net.UDPAddr)
bindPort := uint16(bindAddr.Port) 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[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 { if _, err := conn.Write(resp); err != nil {
return return
} }
+41
View File
@@ -1,6 +1,7 @@
package xray package xray
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
@@ -9,6 +10,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"time" "time"
statsService "github.com/xtls/xray-core/app/stats/command"
) )
// TestXrayAPI_E2E exercises the gRPC hot-apply surface (outbounds, inbounds, // 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) t.Fatalf("api init: %v", err)
} }
defer api.Close() defer api.Close()
waitForXrayAPIReady(t, &api, apiPort)
// --- outbounds --- // --- outbounds ---
socksOutbound := []byte(`{"protocol":"socks","settings":{"servers":[{"address":"127.0.0.1","port":10808}]},"tag":"test-out"}`) 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) 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:
}
}
}
+1
View File
@@ -111,6 +111,7 @@ func startE2ECore(t *testing.T, inbounds []any) *e2eCore {
t.Fatalf("api init: %v", err) t.Fatalf("api init: %v", err)
} }
t.Cleanup(api.Close) t.Cleanup(api.Close)
waitForXrayAPIReady(t, api, apiPort)
return &e2eCore{api: api, cmd: cmd, port: apiPort} return &e2eCore{api: api, cmd: cmd, port: apiPort}
} }