relay_e2e_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. package amneziawgnet
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "net/netip"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "testing"
  13. "time"
  14. awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
  15. "github.com/amnezia-vpn/amneziawg-go/v3/device"
  16. "github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
  17. "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
  18. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  19. "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  20. )
  21. // TestSocksRelayAgainstRealXray is Phase 2's real end-to-end proof: a
  22. // genuine amneziawg-go client completes a real handshake against a Device
  23. // built by NewDevice, dials a real TCP echo server and sends a real UDP
  24. // echo datagram, and this package's own AttachTCPForwarder/AttachUDPHandler
  25. // handlers relay both through RelayTCP/UDPRelay into an *actual xray-core
  26. // process* (not a mock) running a SOCKS5 inbound built by
  27. // SocksInboundSettings. Verifies real data round-trips on both protocols,
  28. // then greps the real process's own debug log for
  29. // "user>>>{email}>>>traffic>>>{up,down}link" -- the same proof Finding 3 of
  30. // the migration plan established manually in Phase 0, now permanent,
  31. // repo-owned test infrastructure. The UDP half in particular is the first
  32. // real test of this package's hand-rolled SOCKS5 UDP ASSOCIATE client
  33. // (relay.go) against an independent, authoritative implementation of the
  34. // protocol rather than a mock this same session wrote.
  35. //
  36. // Skipped unless XRAY_E2E_BINARY points at an xray executable built from
  37. // the same xray-core version as go.mod, matching internal/xray's own
  38. // TestXrayAPI_E2E convention:
  39. //
  40. // go install github.com/xtls/xray-core/main@<version from go.mod>
  41. // XRAY_E2E_BINARY=$GOBIN/main go test ./internal/amneziawgnet -run TestSocksRelayAgainstRealXray -v
  42. func TestSocksRelayAgainstRealXray(t *testing.T) {
  43. bin := os.Getenv("XRAY_E2E_BINARY")
  44. if bin == "" {
  45. t.Skip("set XRAY_E2E_BINARY to an xray binary to run this test")
  46. }
  47. localIP, ok := firstNonLoopbackIPv4()
  48. if !ok {
  49. t.Skip("no non-loopback IPv4 address available on this host")
  50. }
  51. const wantEmail = "[email protected]"
  52. const socksPassword = "loopback-only-not-a-real-secret"
  53. // --- real TCP + UDP echo servers on a real, non-loopback address ---
  54. // (dialing 127.0.0.1 as a tunnel-internal destination hangs -- gVisor
  55. // won't route loopback out an arbitrary NIC -- so the client dials
  56. // localIP instead; it must still be a *real* address since the actual
  57. // relay leg is a genuine OS-level dial from the xray-core process, not
  58. // anything inside the tunnel's virtual netstack.)
  59. tcpEcho, tcpEchoAddr := startTCPEcho(t, localIP)
  60. defer tcpEcho.Close()
  61. udpEcho, udpEchoAddr := startUDPEcho(t, localIP)
  62. defer udpEcho.Close()
  63. // --- real embedded AmneziaWG server + client, same shape as Phase 1's tests ---
  64. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  65. if err != nil {
  66. t.Fatalf("generate server keypair: %v", err)
  67. }
  68. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  69. if err != nil {
  70. t.Fatalf("generate client keypair: %v", err)
  71. }
  72. const listenPort = 58715
  73. inst := amneziawg.Instance{
  74. Id: 4,
  75. InterfaceName: "awgtest4",
  76. ListenPort: listenPort,
  77. PrivateKey: serverPriv,
  78. PublicKey: serverPub,
  79. Address: []string{"10.204.0.1/24"},
  80. MTU: 1420,
  81. Obfuscation: amneziawg.Obfuscation31{
  82. Jc: 4, Jmin: 40, Jmax: 70,
  83. S1: 20, S2: 30, S3: 20, S4: 20,
  84. },
  85. Peers: []amneziawg.Peer{{
  86. Email: wantEmail,
  87. PublicKey: clientPub,
  88. AllowedIPs: []string{"10.204.0.2/32"},
  89. }},
  90. }
  91. dev, err := newUnconfiguredDevice(inst, DeviceOptions{})
  92. if err != nil {
  93. t.Fatalf("newUnconfiguredDevice: %v", err)
  94. }
  95. defer dev.Close()
  96. idx := NewPeerIndex(inst.Peers)
  97. // --- real xray-core process with a SOCKS5 inbound built by this package ---
  98. socksPort := freePort(t)
  99. settingsJSON, err := SocksInboundSettings([]string{wantEmail}, socksPassword)
  100. if err != nil {
  101. t.Fatalf("SocksInboundSettings: %v", err)
  102. }
  103. var rawSettings any
  104. if err := json.Unmarshal(settingsJSON, &rawSettings); err != nil {
  105. t.Fatalf("unmarshal generated SOCKS5 settings: %v", err)
  106. }
  107. xrayCfg := map[string]any{
  108. "log": map[string]any{"loglevel": "debug"},
  109. "inbounds": []any{
  110. map[string]any{
  111. "listen": "127.0.0.1",
  112. "port": socksPort,
  113. "protocol": "socks",
  114. "settings": rawSettings,
  115. "tag": "awg-e2e-socks",
  116. },
  117. },
  118. "outbounds": []any{
  119. map[string]any{"protocol": "freedom", "settings": map[string]any{}, "tag": "direct"},
  120. },
  121. "policy": map[string]any{
  122. "levels": map[string]any{
  123. "0": map[string]any{"statsUserUplink": true, "statsUserDownlink": true},
  124. },
  125. },
  126. "stats": map[string]any{},
  127. }
  128. cfgBytes, err := json.MarshalIndent(xrayCfg, "", " ")
  129. if err != nil {
  130. t.Fatalf("marshal xray config: %v", err)
  131. }
  132. cfgPath := filepath.Join(t.TempDir(), "config.json")
  133. if err := os.WriteFile(cfgPath, cfgBytes, 0o644); err != nil {
  134. t.Fatalf("write xray config: %v", err)
  135. }
  136. var xrayLog syncBuffer
  137. cmd := exec.Command(bin, "-c", cfgPath)
  138. cmd.Stdout = &xrayLog
  139. cmd.Stderr = &xrayLog
  140. if err := cmd.Start(); err != nil {
  141. t.Fatalf("start xray: %v", err)
  142. }
  143. defer func() {
  144. _ = cmd.Process.Kill()
  145. _, _ = cmd.Process.Wait()
  146. }()
  147. waitForPort(t, socksPort)
  148. socksAddr := fmt.Sprintf("127.0.0.1:%d", socksPort)
  149. relay := SocksRelay{Addr: socksAddr, Password: socksPassword}
  150. udpRelay := NewUDPRelay(relay, dev.Stack)
  151. defer udpRelay.Close()
  152. AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
  153. srcAddrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String())
  154. if err != nil {
  155. conn.Close()
  156. return
  157. }
  158. peer, ok := idx.Lookup(srcAddrPort.Addr().Unmap())
  159. if !ok {
  160. conn.Close()
  161. return
  162. }
  163. relay.RelayTCP(conn, peer.Email, dest)
  164. })
  165. AttachUDPHandler(dev.Stack, func(src, dst netip.AddrPort, payload []byte) {
  166. peer, ok := idx.Lookup(src.Addr())
  167. if !ok {
  168. return
  169. }
  170. udpRelay.Handle(src, dst, peer.Email, payload)
  171. })
  172. // Configure (IpcSet) must come after both attaches -- see
  173. // newUnconfiguredDevice's doc comment.
  174. if err := dev.Configure(inst, DeviceOptions{}); err != nil {
  175. t.Fatalf("Configure: %v", err)
  176. }
  177. // --- real client, real handshake, real traffic through the whole chain ---
  178. clientTun, clientNet, err := netstack.CreateNetTUN(
  179. []netip.Addr{netip.MustParseAddr("10.204.0.2")},
  180. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  181. if err != nil {
  182. t.Fatalf("client CreateNetTUN: %v", err)
  183. }
  184. clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
  185. defer clientDev.Close()
  186. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  187. if err != nil {
  188. t.Fatalf("client key to hex: %v", err)
  189. }
  190. serverPubHex, err := wireguard.KeyToHex(serverPub)
  191. if err != nil {
  192. t.Fatalf("server key to hex: %v", err)
  193. }
  194. clientConf := fmt.Sprintf(
  195. "private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
  196. clientPrivHex, serverPubHex, listenPort)
  197. if err := clientDev.IpcSet(clientConf); err != nil {
  198. t.Fatalf("client IpcSet: %v", err)
  199. }
  200. if err := clientDev.Up(); err != nil {
  201. t.Fatalf("client Up: %v", err)
  202. }
  203. // TCP round trip.
  204. const tcpMsg = "hello over amneziawgnet+socks5+xray"
  205. dialDeadline := time.Now().Add(10 * time.Second)
  206. var tcpConn interface {
  207. Write([]byte) (int, error)
  208. Read([]byte) (int, error)
  209. Close() error
  210. }
  211. for {
  212. c, dialErr := clientNet.DialContext(t.Context(), "tcp", tcpEchoAddr.String())
  213. if dialErr == nil {
  214. tcpConn = c
  215. break
  216. }
  217. if time.Now().After(dialDeadline) {
  218. t.Fatalf("client TCP dial via tunnel never succeeded: %v", dialErr)
  219. }
  220. time.Sleep(150 * time.Millisecond)
  221. }
  222. defer tcpConn.Close()
  223. if _, err := tcpConn.Write([]byte(tcpMsg)); err != nil {
  224. t.Fatalf("client TCP write: %v", err)
  225. }
  226. tcpBuf := make([]byte, len(tcpMsg))
  227. if _, err := readFull(tcpConn, tcpBuf, 10*time.Second); err != nil {
  228. t.Fatalf("client TCP read: %v", err)
  229. }
  230. if string(tcpBuf) != tcpMsg {
  231. t.Errorf("TCP echo = %q, want %q", tcpBuf, tcpMsg)
  232. }
  233. // UDP round trip.
  234. const udpMsg = "hello-udp-over-socks5"
  235. uconn, err := clientNet.DialUDPAddrPort(netip.AddrPort{}, udpEchoAddr)
  236. if err != nil {
  237. t.Fatalf("client DialUDPAddrPort: %v", err)
  238. }
  239. defer uconn.Close()
  240. udpDeadline := time.Now().Add(10 * time.Second)
  241. var udpBuf [256]byte
  242. var gotUDP string
  243. for time.Now().Before(udpDeadline) {
  244. _ = uconn.SetWriteDeadline(time.Now().Add(300 * time.Millisecond))
  245. if _, err := uconn.Write([]byte(udpMsg)); err != nil {
  246. continue
  247. }
  248. _ = uconn.SetReadDeadline(time.Now().Add(300 * time.Millisecond))
  249. n, err := uconn.Read(udpBuf[:])
  250. if err == nil {
  251. gotUDP = string(udpBuf[:n])
  252. break
  253. }
  254. }
  255. if gotUDP != udpMsg {
  256. t.Fatalf("UDP echo = %q, want %q (xray log follows)\n%s", gotUDP, udpMsg, xrayLog.String())
  257. }
  258. // Real per-peer stats attribution: stop xray so its log is complete, then
  259. // look for both directions' counters keyed by the peer's real email --
  260. // the exact proof Finding 3 established manually in Phase 0.
  261. _ = cmd.Process.Kill()
  262. _, _ = cmd.Process.Wait()
  263. log := xrayLog.String()
  264. wantUp := fmt.Sprintf("user>>>%s>>>traffic>>>uplink", wantEmail)
  265. wantDown := fmt.Sprintf("user>>>%s>>>traffic>>>downlink", wantEmail)
  266. if !strings.Contains(log, wantUp) {
  267. t.Errorf("xray log missing uplink stats counter %q\nfull log:\n%s", wantUp, log)
  268. }
  269. if !strings.Contains(log, wantDown) {
  270. t.Errorf("xray log missing downlink stats counter %q\nfull log:\n%s", wantDown, log)
  271. }
  272. }
  273. // TestManagerEnsureAutomaticallyWiresRelay is Phase 3's own real proof: unlike
  274. // TestSocksRelayAgainstRealXray above (which builds a Device and attaches
  275. // RelayTCP/UDPRelay by hand), this drives everything through the public
  276. // Manager.Ensure entry point the real app actually calls -- confirming
  277. // ensureLocked's own forwarder/UDP-handler attachment (added this phase)
  278. // really does relay a fresh Device's traffic into Xray with zero manual
  279. // wiring from the caller. Uses the exact port/password
  280. // (SOCKSPortForInbound/SocksPassword) the Manager computes internally, so
  281. // this only passes if that internal derivation and the externally-visible
  282. // contract genuinely agree.
  283. func TestManagerEnsureAutomaticallyWiresRelay(t *testing.T) {
  284. bin := os.Getenv("XRAY_E2E_BINARY")
  285. if bin == "" {
  286. t.Skip("set XRAY_E2E_BINARY to an xray binary to run this test")
  287. }
  288. localIP, ok := firstNonLoopbackIPv4()
  289. if !ok {
  290. t.Skip("no non-loopback IPv4 address available on this host")
  291. }
  292. const wantEmail = "[email protected]"
  293. const listenPort = 58716
  294. const inboundID = 5
  295. tcpEcho, tcpEchoAddr := startTCPEcho(t, localIP)
  296. defer tcpEcho.Close()
  297. serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
  298. if err != nil {
  299. t.Fatalf("generate server keypair: %v", err)
  300. }
  301. clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
  302. if err != nil {
  303. t.Fatalf("generate client keypair: %v", err)
  304. }
  305. inst := amneziawg.Instance{
  306. Id: inboundID,
  307. InterfaceName: "awgtest5",
  308. ListenPort: listenPort,
  309. PrivateKey: serverPriv,
  310. PublicKey: serverPub,
  311. Address: []string{"10.205.0.1/24"},
  312. MTU: 1420,
  313. Obfuscation: amneziawg.Obfuscation31{
  314. Jc: 4, Jmin: 40, Jmax: 70,
  315. S1: 20, S2: 30, S3: 20, S4: 20,
  316. },
  317. Peers: []amneziawg.Peer{{
  318. Email: wantEmail,
  319. PublicKey: clientPub,
  320. AllowedIPs: []string{"10.205.0.2/32"},
  321. }},
  322. }
  323. // A real xray-core process with a SOCKS5 inbound at exactly the port and
  324. // password ensureLocked will derive on its own for this instance --
  325. // SocksPassword() is cached (sync.Once), so calling it here first and
  326. // again inside Manager.Ensure below returns the identical value.
  327. socksPort := SOCKSPortForInbound(inboundID)
  328. password := SocksPassword()
  329. settingsJSON, err := SocksInboundSettings([]string{wantEmail}, password)
  330. if err != nil {
  331. t.Fatalf("SocksInboundSettings: %v", err)
  332. }
  333. var rawSettings any
  334. if err := json.Unmarshal(settingsJSON, &rawSettings); err != nil {
  335. t.Fatalf("unmarshal generated SOCKS5 settings: %v", err)
  336. }
  337. xrayCfg := map[string]any{
  338. "log": map[string]any{"loglevel": "debug"},
  339. "inbounds": []any{
  340. map[string]any{
  341. "listen": "127.0.0.1",
  342. "port": socksPort,
  343. "protocol": "socks",
  344. "settings": rawSettings,
  345. "tag": "awg-e2e-manager",
  346. },
  347. },
  348. "outbounds": []any{
  349. map[string]any{"protocol": "freedom", "settings": map[string]any{}, "tag": "direct"},
  350. },
  351. "policy": map[string]any{
  352. "levels": map[string]any{
  353. "0": map[string]any{"statsUserUplink": true, "statsUserDownlink": true},
  354. },
  355. },
  356. "stats": map[string]any{},
  357. }
  358. cfgBytes, err := json.MarshalIndent(xrayCfg, "", " ")
  359. if err != nil {
  360. t.Fatalf("marshal xray config: %v", err)
  361. }
  362. cfgPath := filepath.Join(t.TempDir(), "config.json")
  363. if err := os.WriteFile(cfgPath, cfgBytes, 0o644); err != nil {
  364. t.Fatalf("write xray config: %v", err)
  365. }
  366. var xrayLog syncBuffer
  367. cmd := exec.Command(bin, "-c", cfgPath)
  368. cmd.Stdout = &xrayLog
  369. cmd.Stderr = &xrayLog
  370. if err := cmd.Start(); err != nil {
  371. t.Fatalf("start xray: %v", err)
  372. }
  373. defer func() {
  374. _ = cmd.Process.Kill()
  375. _, _ = cmd.Process.Wait()
  376. }()
  377. waitForPort(t, socksPort)
  378. // A throwaway Manager, not the process-wide singleton, so this test
  379. // doesn't interact with any other test's state.
  380. m := &Manager{ifaces: map[int]*managed{}}
  381. defer m.StopAll()
  382. if err := m.Ensure(Desired{Instance: inst}); err != nil {
  383. t.Fatalf("Manager.Ensure: %v", err)
  384. }
  385. dev, _, ok := m.Lookup(inboundID)
  386. if !ok {
  387. t.Fatal("Lookup after Ensure: not found")
  388. }
  389. defer dev.Close() // StopAll would also do this; explicit for clarity
  390. clientTun, clientNet, err := netstack.CreateNetTUN(
  391. []netip.Addr{netip.MustParseAddr("10.205.0.2")},
  392. []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
  393. if err != nil {
  394. t.Fatalf("client CreateNetTUN: %v", err)
  395. }
  396. clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
  397. defer clientDev.Close()
  398. clientPrivHex, err := wireguard.KeyToHex(clientPriv)
  399. if err != nil {
  400. t.Fatalf("client key to hex: %v", err)
  401. }
  402. serverPubHex, err := wireguard.KeyToHex(serverPub)
  403. if err != nil {
  404. t.Fatalf("server key to hex: %v", err)
  405. }
  406. clientConf := fmt.Sprintf(
  407. "private_key=%s\njc=4\njmin=40\njmax=70\ns1=20\ns2=30\ns3=20\ns4=20\npublic_key=%s\nendpoint=127.0.0.1:%d\nallowed_ip=0.0.0.0/0\n",
  408. clientPrivHex, serverPubHex, listenPort)
  409. if err := clientDev.IpcSet(clientConf); err != nil {
  410. t.Fatalf("client IpcSet: %v", err)
  411. }
  412. if err := clientDev.Up(); err != nil {
  413. t.Fatalf("client Up: %v", err)
  414. }
  415. const tcpMsg = "hello via Manager.Ensure's automatic relay wiring"
  416. dialDeadline := time.Now().Add(10 * time.Second)
  417. var conn net.Conn
  418. for {
  419. c, dialErr := clientNet.DialContext(t.Context(), "tcp", tcpEchoAddr.String())
  420. if dialErr == nil {
  421. conn = c
  422. break
  423. }
  424. if time.Now().After(dialDeadline) {
  425. t.Fatalf("client TCP dial via tunnel never succeeded: %v", dialErr)
  426. }
  427. time.Sleep(150 * time.Millisecond)
  428. }
  429. defer conn.Close()
  430. if _, err := conn.Write([]byte(tcpMsg)); err != nil {
  431. t.Fatalf("client TCP write: %v", err)
  432. }
  433. buf := make([]byte, len(tcpMsg))
  434. if _, err := readFull(conn, buf, 10*time.Second); err != nil {
  435. t.Fatalf("client TCP read: %v", err)
  436. }
  437. if string(buf) != tcpMsg {
  438. t.Errorf("TCP echo = %q, want %q", buf, tcpMsg)
  439. }
  440. _ = cmd.Process.Kill()
  441. _, _ = cmd.Process.Wait()
  442. log := xrayLog.String()
  443. wantUp := fmt.Sprintf("user>>>%s>>>traffic>>>uplink", wantEmail)
  444. if !strings.Contains(log, wantUp) {
  445. t.Errorf("xray log missing uplink stats counter %q (Manager.Ensure's automatic relay wiring may not be attributing traffic correctly)\nfull log:\n%s", wantUp, log)
  446. }
  447. }
  448. // firstNonLoopbackIPv4 finds a real, locally-bound IPv4 address suitable as
  449. // a relay-reachable test destination.
  450. func firstNonLoopbackIPv4() (netip.Addr, bool) {
  451. addrs, err := net.InterfaceAddrs()
  452. if err != nil {
  453. return netip.Addr{}, false
  454. }
  455. for _, a := range addrs {
  456. ipNet, ok := a.(*net.IPNet)
  457. if !ok || ipNet.IP.IsLoopback() {
  458. continue
  459. }
  460. if v4 := ipNet.IP.To4(); v4 != nil {
  461. addr, ok := netip.AddrFromSlice(v4)
  462. if ok {
  463. return addr, true
  464. }
  465. }
  466. }
  467. return netip.Addr{}, false
  468. }
  469. func startTCPEcho(t *testing.T, addr netip.Addr) (io interface{ Close() error }, ap netip.AddrPort) {
  470. t.Helper()
  471. ln, err := net.Listen("tcp", net.JoinHostPort(addr.String(), "0"))
  472. if err != nil {
  473. t.Fatalf("start TCP echo listener: %v", err)
  474. }
  475. go func() {
  476. for {
  477. c, err := ln.Accept()
  478. if err != nil {
  479. return
  480. }
  481. go func() {
  482. defer c.Close()
  483. buf := make([]byte, 4096)
  484. for {
  485. n, err := c.Read(buf)
  486. if n > 0 {
  487. if _, werr := c.Write(buf[:n]); werr != nil {
  488. return
  489. }
  490. }
  491. if err != nil {
  492. return
  493. }
  494. }
  495. }()
  496. }
  497. }()
  498. port := ln.Addr().(*net.TCPAddr).Port
  499. return ln, netip.AddrPortFrom(addr, uint16(port))
  500. }
  501. func startUDPEcho(t *testing.T, addr netip.Addr) (io interface{ Close() error }, ap netip.AddrPort) {
  502. t.Helper()
  503. pc, err := net.ListenPacket("udp", net.JoinHostPort(addr.String(), "0"))
  504. if err != nil {
  505. t.Fatalf("start UDP echo listener: %v", err)
  506. }
  507. go func() {
  508. buf := make([]byte, 4096)
  509. for {
  510. n, raddr, err := pc.ReadFrom(buf)
  511. if err != nil {
  512. return
  513. }
  514. if _, err := pc.WriteTo(buf[:n], raddr); err != nil {
  515. return
  516. }
  517. }
  518. }()
  519. port := pc.LocalAddr().(*net.UDPAddr).Port
  520. return pc, netip.AddrPortFrom(addr, uint16(port))
  521. }
  522. // readFull reads exactly len(buf) bytes or fails after timeout, since
  523. // gonet.TCPConn (and net.Conn generally) may return short reads.
  524. func readFull(r interface{ Read([]byte) (int, error) }, buf []byte, timeout time.Duration) (int, error) {
  525. deadline := time.Now().Add(timeout)
  526. total := 0
  527. for total < len(buf) {
  528. if time.Now().After(deadline) {
  529. return total, fmt.Errorf("timed out after reading %d/%d bytes", total, len(buf))
  530. }
  531. n, err := r.Read(buf[total:])
  532. total += n
  533. if err != nil {
  534. return total, err
  535. }
  536. }
  537. return total, nil
  538. }
  539. // syncBuffer is a concurrency-safe bytes buffer for capturing a subprocess's
  540. // combined stdout/stderr while the test may read it from another goroutine.
  541. type syncBuffer struct {
  542. mu sync.Mutex
  543. buf strings.Builder
  544. }
  545. func (s *syncBuffer) Write(p []byte) (int, error) {
  546. s.mu.Lock()
  547. defer s.mu.Unlock()
  548. return s.buf.Write(p)
  549. }
  550. func (s *syncBuffer) String() string {
  551. s.mu.Lock()
  552. defer s.mu.Unlock()
  553. return s.buf.String()
  554. }
  555. func freePort(t *testing.T) int {
  556. t.Helper()
  557. l, err := net.Listen("tcp", "127.0.0.1:0")
  558. if err != nil {
  559. t.Fatal(err)
  560. }
  561. defer l.Close()
  562. return l.Addr().(*net.TCPAddr).Port
  563. }
  564. func waitForPort(t *testing.T, port int) {
  565. t.Helper()
  566. deadline := time.Now().Add(15 * time.Second)
  567. addr := fmt.Sprintf("127.0.0.1:%d", port)
  568. for time.Now().Before(deadline) {
  569. conn, err := net.DialTimeout("tcp", addr, time.Second)
  570. if err == nil {
  571. conn.Close()
  572. return
  573. }
  574. time.Sleep(200 * time.Millisecond)
  575. }
  576. t.Fatalf("xray port %d did not open in time", port)
  577. }