mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-05 09:57:14 +00:00
124665c5ef
Manager.ensureLocked now attaches AttachTCPForwarder/AttachUDPHandler to every Device it builds, relaying into that instance's own loopback SOCKS5 inbound automatically -- no caller needs to know relay.go exists at all. Peer identity is re-looked-up via Manager.Lookup on every connection rather than captured once at attach time, so a reconfigure-in-place (peers added/ removed without a full rebuild) doesn't leave the forwarder working off a stale peer index. Added TestManagerEnsureAutomaticallyWiresRelay: drives this through the real Manager.Ensure entry point (not manual wiring like the existing relay_e2e_test.go) against a real xray-core process, confirming the automatic attachment and the port/password Manager derives internally actually agree with what a real SOCKS5 inbound expects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
612 lines
18 KiB
Go
612 lines
18 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
awgconn "github.com/amnezia-vpn/amneziawg-go/v3/conn"
|
|
"github.com/amnezia-vpn/amneziawg-go/v3/device"
|
|
"github.com/amnezia-vpn/amneziawg-go/v3/tun/netstack"
|
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
// TestSocksRelayAgainstRealXray is Phase 2's real end-to-end proof: a
|
|
// genuine amneziawg-go client completes a real handshake against a Device
|
|
// built by NewDevice, dials a real TCP echo server and sends a real UDP
|
|
// echo datagram, and this package's own AttachTCPForwarder/AttachUDPHandler
|
|
// handlers relay both through RelayTCP/UDPRelay into an *actual xray-core
|
|
// process* (not a mock) running a SOCKS5 inbound built by
|
|
// SocksInboundSettings. Verifies real data round-trips on both protocols,
|
|
// then greps the real process's own debug log for
|
|
// "user>>>{email}>>>traffic>>>{up,down}link" -- the same proof Finding 3 of
|
|
// the migration plan established manually in Phase 0, now permanent,
|
|
// repo-owned test infrastructure. The UDP half in particular is the first
|
|
// real test of this package's hand-rolled SOCKS5 UDP ASSOCIATE client
|
|
// (relay.go) against an independent, authoritative implementation of the
|
|
// protocol rather than a mock this same session wrote.
|
|
//
|
|
// Skipped unless XRAY_E2E_BINARY points at an xray executable built from
|
|
// the same xray-core version as go.mod, matching internal/xray's own
|
|
// TestXrayAPI_E2E convention:
|
|
//
|
|
// go install github.com/xtls/xray-core/main@<version from go.mod>
|
|
// XRAY_E2E_BINARY=$GOBIN/main go test ./internal/amneziawgnet -run TestSocksRelayAgainstRealXray -v
|
|
func TestSocksRelayAgainstRealXray(t *testing.T) {
|
|
bin := os.Getenv("XRAY_E2E_BINARY")
|
|
if bin == "" {
|
|
t.Skip("set XRAY_E2E_BINARY to an xray binary to run this test")
|
|
}
|
|
|
|
localIP, ok := firstNonLoopbackIPv4()
|
|
if !ok {
|
|
t.Skip("no non-loopback IPv4 address available on this host")
|
|
}
|
|
|
|
const wantEmail = "e2e-peer@example.com"
|
|
const socksPassword = "loopback-only-not-a-real-secret"
|
|
|
|
// --- real TCP + UDP echo servers on a real, non-loopback address ---
|
|
// (dialing 127.0.0.1 as a tunnel-internal destination hangs -- gVisor
|
|
// won't route loopback out an arbitrary NIC -- so the client dials
|
|
// localIP instead; it must still be a *real* address since the actual
|
|
// relay leg is a genuine OS-level dial from the xray-core process, not
|
|
// anything inside the tunnel's virtual netstack.)
|
|
tcpEcho, tcpEchoAddr := startTCPEcho(t, localIP)
|
|
defer tcpEcho.Close()
|
|
udpEcho, udpEchoAddr := startUDPEcho(t, localIP)
|
|
defer udpEcho.Close()
|
|
|
|
// --- real embedded AmneziaWG server + client, same shape as Phase 1's tests ---
|
|
serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatalf("generate server keypair: %v", err)
|
|
}
|
|
clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatalf("generate client keypair: %v", err)
|
|
}
|
|
|
|
const listenPort = 58715
|
|
inst := amneziawg.Instance{
|
|
Id: 4,
|
|
InterfaceName: "awgtest4",
|
|
ListenPort: listenPort,
|
|
PrivateKey: serverPriv,
|
|
PublicKey: serverPub,
|
|
Address: []string{"10.204.0.1/24"},
|
|
MTU: 1420,
|
|
Obfuscation: amneziawg.Obfuscation20{
|
|
Jc: 4, Jmin: 40, Jmax: 70,
|
|
S1: 20, S2: 30, S3: 20, S4: 20,
|
|
},
|
|
Peers: []amneziawg.Peer{{
|
|
Email: wantEmail,
|
|
PublicKey: clientPub,
|
|
AllowedIPs: []string{"10.204.0.2/32"},
|
|
}},
|
|
}
|
|
dev, err := NewDevice(inst, DeviceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("NewDevice: %v", err)
|
|
}
|
|
defer dev.Close()
|
|
idx := NewPeerIndex(inst.Peers)
|
|
|
|
// --- real xray-core process with a SOCKS5 inbound built by this package ---
|
|
socksPort := freePort(t)
|
|
settingsJSON, err := SocksInboundSettings([]string{wantEmail}, socksPassword)
|
|
if err != nil {
|
|
t.Fatalf("SocksInboundSettings: %v", err)
|
|
}
|
|
var rawSettings any
|
|
if err := json.Unmarshal(settingsJSON, &rawSettings); err != nil {
|
|
t.Fatalf("unmarshal generated SOCKS5 settings: %v", err)
|
|
}
|
|
xrayCfg := map[string]any{
|
|
"log": map[string]any{"loglevel": "debug"},
|
|
"inbounds": []any{
|
|
map[string]any{
|
|
"listen": "127.0.0.1",
|
|
"port": socksPort,
|
|
"protocol": "socks",
|
|
"settings": rawSettings,
|
|
"tag": "awg-e2e-socks",
|
|
},
|
|
},
|
|
"outbounds": []any{
|
|
map[string]any{"protocol": "freedom", "settings": map[string]any{}, "tag": "direct"},
|
|
},
|
|
"policy": map[string]any{
|
|
"levels": map[string]any{
|
|
"0": map[string]any{"statsUserUplink": true, "statsUserDownlink": true},
|
|
},
|
|
},
|
|
"stats": map[string]any{},
|
|
}
|
|
cfgBytes, err := json.MarshalIndent(xrayCfg, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("marshal xray config: %v", err)
|
|
}
|
|
cfgPath := filepath.Join(t.TempDir(), "config.json")
|
|
if err := os.WriteFile(cfgPath, cfgBytes, 0o644); err != nil {
|
|
t.Fatalf("write xray config: %v", err)
|
|
}
|
|
|
|
var xrayLog syncBuffer
|
|
cmd := exec.Command(bin, "-c", cfgPath)
|
|
cmd.Stdout = &xrayLog
|
|
cmd.Stderr = &xrayLog
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start xray: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}()
|
|
waitForPort(t, socksPort)
|
|
|
|
socksAddr := fmt.Sprintf("127.0.0.1:%d", socksPort)
|
|
relay := SocksRelay{Addr: socksAddr, Password: socksPassword}
|
|
udpRelay := NewUDPRelay(relay, dev.Stack)
|
|
defer udpRelay.Close()
|
|
|
|
AttachTCPForwarder(dev.Stack, func(conn *gonet.TCPConn, dest netip.AddrPort) {
|
|
srcAddrPort, err := netip.ParseAddrPort(conn.RemoteAddr().String())
|
|
if err != nil {
|
|
conn.Close()
|
|
return
|
|
}
|
|
peer, ok := idx.Lookup(srcAddrPort.Addr().Unmap())
|
|
if !ok {
|
|
conn.Close()
|
|
return
|
|
}
|
|
relay.RelayTCP(conn, peer.Email, dest)
|
|
})
|
|
AttachUDPHandler(dev.Stack, func(src, dst netip.AddrPort, payload []byte) {
|
|
peer, ok := idx.Lookup(src.Addr())
|
|
if !ok {
|
|
return
|
|
}
|
|
udpRelay.Handle(src, dst, peer.Email, payload)
|
|
})
|
|
|
|
// --- real client, real handshake, real traffic through the whole chain ---
|
|
clientTun, clientNet, err := netstack.CreateNetTUN(
|
|
[]netip.Addr{netip.MustParseAddr("10.204.0.2")},
|
|
[]netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
|
|
if err != nil {
|
|
t.Fatalf("client CreateNetTUN: %v", err)
|
|
}
|
|
clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
|
|
defer clientDev.Close()
|
|
|
|
clientPrivHex, err := wireguard.KeyToHex(clientPriv)
|
|
if err != nil {
|
|
t.Fatalf("client key to hex: %v", err)
|
|
}
|
|
serverPubHex, err := wireguard.KeyToHex(serverPub)
|
|
if err != nil {
|
|
t.Fatalf("server key to hex: %v", err)
|
|
}
|
|
clientConf := fmt.Sprintf(
|
|
"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",
|
|
clientPrivHex, serverPubHex, listenPort)
|
|
if err := clientDev.IpcSet(clientConf); err != nil {
|
|
t.Fatalf("client IpcSet: %v", err)
|
|
}
|
|
if err := clientDev.Up(); err != nil {
|
|
t.Fatalf("client Up: %v", err)
|
|
}
|
|
|
|
// TCP round trip.
|
|
const tcpMsg = "hello over amneziawgnet+socks5+xray"
|
|
dialDeadline := time.Now().Add(10 * time.Second)
|
|
var tcpConn interface {
|
|
Write([]byte) (int, error)
|
|
Read([]byte) (int, error)
|
|
Close() error
|
|
}
|
|
for {
|
|
c, dialErr := clientNet.DialContext(t.Context(), "tcp", tcpEchoAddr.String())
|
|
if dialErr == nil {
|
|
tcpConn = c
|
|
break
|
|
}
|
|
if time.Now().After(dialDeadline) {
|
|
t.Fatalf("client TCP dial via tunnel never succeeded: %v", dialErr)
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
}
|
|
defer tcpConn.Close()
|
|
if _, err := tcpConn.Write([]byte(tcpMsg)); err != nil {
|
|
t.Fatalf("client TCP write: %v", err)
|
|
}
|
|
tcpBuf := make([]byte, len(tcpMsg))
|
|
if _, err := readFull(tcpConn, tcpBuf, 10*time.Second); err != nil {
|
|
t.Fatalf("client TCP read: %v", err)
|
|
}
|
|
if string(tcpBuf) != tcpMsg {
|
|
t.Errorf("TCP echo = %q, want %q", tcpBuf, tcpMsg)
|
|
}
|
|
|
|
// UDP round trip.
|
|
const udpMsg = "hello-udp-over-socks5"
|
|
uconn, err := clientNet.DialUDPAddrPort(netip.AddrPort{}, udpEchoAddr)
|
|
if err != nil {
|
|
t.Fatalf("client DialUDPAddrPort: %v", err)
|
|
}
|
|
defer uconn.Close()
|
|
udpDeadline := time.Now().Add(10 * time.Second)
|
|
var udpBuf [256]byte
|
|
var gotUDP string
|
|
for time.Now().Before(udpDeadline) {
|
|
_ = uconn.SetWriteDeadline(time.Now().Add(300 * time.Millisecond))
|
|
if _, err := uconn.Write([]byte(udpMsg)); err != nil {
|
|
continue
|
|
}
|
|
_ = uconn.SetReadDeadline(time.Now().Add(300 * time.Millisecond))
|
|
n, err := uconn.Read(udpBuf[:])
|
|
if err == nil {
|
|
gotUDP = string(udpBuf[:n])
|
|
break
|
|
}
|
|
}
|
|
if gotUDP != udpMsg {
|
|
t.Fatalf("UDP echo = %q, want %q (xray log follows)\n%s", gotUDP, udpMsg, xrayLog.String())
|
|
}
|
|
|
|
// Real per-peer stats attribution: stop xray so its log is complete, then
|
|
// look for both directions' counters keyed by the peer's real email --
|
|
// the exact proof Finding 3 established manually in Phase 0.
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
log := xrayLog.String()
|
|
wantUp := fmt.Sprintf("user>>>%s>>>traffic>>>uplink", wantEmail)
|
|
wantDown := fmt.Sprintf("user>>>%s>>>traffic>>>downlink", wantEmail)
|
|
if !strings.Contains(log, wantUp) {
|
|
t.Errorf("xray log missing uplink stats counter %q\nfull log:\n%s", wantUp, log)
|
|
}
|
|
if !strings.Contains(log, wantDown) {
|
|
t.Errorf("xray log missing downlink stats counter %q\nfull log:\n%s", wantDown, log)
|
|
}
|
|
}
|
|
|
|
// TestManagerEnsureAutomaticallyWiresRelay is Phase 3's own real proof: unlike
|
|
// TestSocksRelayAgainstRealXray above (which builds a Device and attaches
|
|
// RelayTCP/UDPRelay by hand), this drives everything through the public
|
|
// Manager.Ensure entry point the real app actually calls -- confirming
|
|
// ensureLocked's own forwarder/UDP-handler attachment (added this phase)
|
|
// really does relay a fresh Device's traffic into Xray with zero manual
|
|
// wiring from the caller. Uses the exact port/password
|
|
// (SOCKSPortForInbound/SocksPassword) the Manager computes internally, so
|
|
// this only passes if that internal derivation and the externally-visible
|
|
// contract genuinely agree.
|
|
func TestManagerEnsureAutomaticallyWiresRelay(t *testing.T) {
|
|
bin := os.Getenv("XRAY_E2E_BINARY")
|
|
if bin == "" {
|
|
t.Skip("set XRAY_E2E_BINARY to an xray binary to run this test")
|
|
}
|
|
localIP, ok := firstNonLoopbackIPv4()
|
|
if !ok {
|
|
t.Skip("no non-loopback IPv4 address available on this host")
|
|
}
|
|
|
|
const wantEmail = "manager-e2e-peer@example.com"
|
|
const listenPort = 58716
|
|
const inboundID = 5
|
|
|
|
tcpEcho, tcpEchoAddr := startTCPEcho(t, localIP)
|
|
defer tcpEcho.Close()
|
|
|
|
serverPriv, serverPub, err := wireguard.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatalf("generate server keypair: %v", err)
|
|
}
|
|
clientPriv, clientPub, err := wireguard.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatalf("generate client keypair: %v", err)
|
|
}
|
|
|
|
inst := amneziawg.Instance{
|
|
Id: inboundID,
|
|
InterfaceName: "awgtest5",
|
|
ListenPort: listenPort,
|
|
PrivateKey: serverPriv,
|
|
PublicKey: serverPub,
|
|
Address: []string{"10.205.0.1/24"},
|
|
MTU: 1420,
|
|
Obfuscation: amneziawg.Obfuscation20{
|
|
Jc: 4, Jmin: 40, Jmax: 70,
|
|
S1: 20, S2: 30, S3: 20, S4: 20,
|
|
},
|
|
Peers: []amneziawg.Peer{{
|
|
Email: wantEmail,
|
|
PublicKey: clientPub,
|
|
AllowedIPs: []string{"10.205.0.2/32"},
|
|
}},
|
|
}
|
|
|
|
// A real xray-core process with a SOCKS5 inbound at exactly the port and
|
|
// password ensureLocked will derive on its own for this instance --
|
|
// SocksPassword() is cached (sync.Once), so calling it here first and
|
|
// again inside Manager.Ensure below returns the identical value.
|
|
socksPort := SOCKSPortForInbound(inboundID)
|
|
password := SocksPassword()
|
|
settingsJSON, err := SocksInboundSettings([]string{wantEmail}, password)
|
|
if err != nil {
|
|
t.Fatalf("SocksInboundSettings: %v", err)
|
|
}
|
|
var rawSettings any
|
|
if err := json.Unmarshal(settingsJSON, &rawSettings); err != nil {
|
|
t.Fatalf("unmarshal generated SOCKS5 settings: %v", err)
|
|
}
|
|
xrayCfg := map[string]any{
|
|
"log": map[string]any{"loglevel": "debug"},
|
|
"inbounds": []any{
|
|
map[string]any{
|
|
"listen": "127.0.0.1",
|
|
"port": socksPort,
|
|
"protocol": "socks",
|
|
"settings": rawSettings,
|
|
"tag": "awg-e2e-manager",
|
|
},
|
|
},
|
|
"outbounds": []any{
|
|
map[string]any{"protocol": "freedom", "settings": map[string]any{}, "tag": "direct"},
|
|
},
|
|
"policy": map[string]any{
|
|
"levels": map[string]any{
|
|
"0": map[string]any{"statsUserUplink": true, "statsUserDownlink": true},
|
|
},
|
|
},
|
|
"stats": map[string]any{},
|
|
}
|
|
cfgBytes, err := json.MarshalIndent(xrayCfg, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("marshal xray config: %v", err)
|
|
}
|
|
cfgPath := filepath.Join(t.TempDir(), "config.json")
|
|
if err := os.WriteFile(cfgPath, cfgBytes, 0o644); err != nil {
|
|
t.Fatalf("write xray config: %v", err)
|
|
}
|
|
|
|
var xrayLog syncBuffer
|
|
cmd := exec.Command(bin, "-c", cfgPath)
|
|
cmd.Stdout = &xrayLog
|
|
cmd.Stderr = &xrayLog
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start xray: %v", err)
|
|
}
|
|
defer func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}()
|
|
waitForPort(t, socksPort)
|
|
|
|
// A throwaway Manager, not the process-wide singleton, so this test
|
|
// doesn't interact with any other test's state.
|
|
m := &Manager{ifaces: map[int]*managed{}}
|
|
defer m.StopAll()
|
|
if err := m.Ensure(Desired{Instance: inst}); err != nil {
|
|
t.Fatalf("Manager.Ensure: %v", err)
|
|
}
|
|
dev, _, ok := m.Lookup(inboundID)
|
|
if !ok {
|
|
t.Fatal("Lookup after Ensure: not found")
|
|
}
|
|
defer dev.Close() // StopAll would also do this; explicit for clarity
|
|
|
|
clientTun, clientNet, err := netstack.CreateNetTUN(
|
|
[]netip.Addr{netip.MustParseAddr("10.205.0.2")},
|
|
[]netip.Addr{netip.MustParseAddr("1.1.1.1")}, 1420)
|
|
if err != nil {
|
|
t.Fatalf("client CreateNetTUN: %v", err)
|
|
}
|
|
clientDev := device.NewDevice(clientTun, awgconn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, ""))
|
|
defer clientDev.Close()
|
|
|
|
clientPrivHex, err := wireguard.KeyToHex(clientPriv)
|
|
if err != nil {
|
|
t.Fatalf("client key to hex: %v", err)
|
|
}
|
|
serverPubHex, err := wireguard.KeyToHex(serverPub)
|
|
if err != nil {
|
|
t.Fatalf("server key to hex: %v", err)
|
|
}
|
|
clientConf := fmt.Sprintf(
|
|
"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",
|
|
clientPrivHex, serverPubHex, listenPort)
|
|
if err := clientDev.IpcSet(clientConf); err != nil {
|
|
t.Fatalf("client IpcSet: %v", err)
|
|
}
|
|
if err := clientDev.Up(); err != nil {
|
|
t.Fatalf("client Up: %v", err)
|
|
}
|
|
|
|
const tcpMsg = "hello via Manager.Ensure's automatic relay wiring"
|
|
dialDeadline := time.Now().Add(10 * time.Second)
|
|
var conn net.Conn
|
|
for {
|
|
c, dialErr := clientNet.DialContext(t.Context(), "tcp", tcpEchoAddr.String())
|
|
if dialErr == nil {
|
|
conn = c
|
|
break
|
|
}
|
|
if time.Now().After(dialDeadline) {
|
|
t.Fatalf("client TCP dial via tunnel never succeeded: %v", dialErr)
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
}
|
|
defer conn.Close()
|
|
if _, err := conn.Write([]byte(tcpMsg)); err != nil {
|
|
t.Fatalf("client TCP write: %v", err)
|
|
}
|
|
buf := make([]byte, len(tcpMsg))
|
|
if _, err := readFull(conn, buf, 10*time.Second); err != nil {
|
|
t.Fatalf("client TCP read: %v", err)
|
|
}
|
|
if string(buf) != tcpMsg {
|
|
t.Errorf("TCP echo = %q, want %q", buf, tcpMsg)
|
|
}
|
|
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
log := xrayLog.String()
|
|
wantUp := fmt.Sprintf("user>>>%s>>>traffic>>>uplink", wantEmail)
|
|
if !strings.Contains(log, wantUp) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// firstNonLoopbackIPv4 finds a real, locally-bound IPv4 address suitable as
|
|
// a relay-reachable test destination.
|
|
func firstNonLoopbackIPv4() (netip.Addr, bool) {
|
|
addrs, err := net.InterfaceAddrs()
|
|
if err != nil {
|
|
return netip.Addr{}, false
|
|
}
|
|
for _, a := range addrs {
|
|
ipNet, ok := a.(*net.IPNet)
|
|
if !ok || ipNet.IP.IsLoopback() {
|
|
continue
|
|
}
|
|
if v4 := ipNet.IP.To4(); v4 != nil {
|
|
addr, ok := netip.AddrFromSlice(v4)
|
|
if ok {
|
|
return addr, true
|
|
}
|
|
}
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
func startTCPEcho(t *testing.T, addr netip.Addr) (io interface{ Close() error }, ap netip.AddrPort) {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", net.JoinHostPort(addr.String(), "0"))
|
|
if err != nil {
|
|
t.Fatalf("start TCP echo listener: %v", err)
|
|
}
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go func() {
|
|
defer c.Close()
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, err := c.Read(buf)
|
|
if n > 0 {
|
|
if _, werr := c.Write(buf[:n]); werr != nil {
|
|
return
|
|
}
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}()
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
return ln, netip.AddrPortFrom(addr, uint16(port))
|
|
}
|
|
|
|
func startUDPEcho(t *testing.T, addr netip.Addr) (io interface{ Close() error }, ap netip.AddrPort) {
|
|
t.Helper()
|
|
pc, err := net.ListenPacket("udp", net.JoinHostPort(addr.String(), "0"))
|
|
if err != nil {
|
|
t.Fatalf("start UDP echo listener: %v", err)
|
|
}
|
|
go func() {
|
|
buf := make([]byte, 4096)
|
|
for {
|
|
n, raddr, err := pc.ReadFrom(buf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if _, err := pc.WriteTo(buf[:n], raddr); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
port := pc.LocalAddr().(*net.UDPAddr).Port
|
|
return pc, netip.AddrPortFrom(addr, uint16(port))
|
|
}
|
|
|
|
// readFull reads exactly len(buf) bytes or fails after timeout, since
|
|
// gonet.TCPConn (and net.Conn generally) may return short reads.
|
|
func readFull(r interface{ Read([]byte) (int, error) }, buf []byte, timeout time.Duration) (int, error) {
|
|
deadline := time.Now().Add(timeout)
|
|
total := 0
|
|
for total < len(buf) {
|
|
if time.Now().After(deadline) {
|
|
return total, fmt.Errorf("timed out after reading %d/%d bytes", total, len(buf))
|
|
}
|
|
n, err := r.Read(buf[total:])
|
|
total += n
|
|
if err != nil {
|
|
return total, err
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
// syncBuffer is a concurrency-safe bytes buffer for capturing a subprocess's
|
|
// combined stdout/stderr while the test may read it from another goroutine.
|
|
type syncBuffer struct {
|
|
mu sync.Mutex
|
|
buf strings.Builder
|
|
}
|
|
|
|
func (s *syncBuffer) Write(p []byte) (int, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.buf.Write(p)
|
|
}
|
|
|
|
func (s *syncBuffer) String() string {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.buf.String()
|
|
}
|
|
|
|
func freePort(t *testing.T) int {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer l.Close()
|
|
return l.Addr().(*net.TCPAddr).Port
|
|
}
|
|
|
|
func waitForPort(t *testing.T, port int) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(15 * time.Second)
|
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
|
for time.Now().Before(deadline) {
|
|
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
|
if err == nil {
|
|
conn.Close()
|
|
return
|
|
}
|
|
time.Sleep(200 * time.Millisecond)
|
|
}
|
|
t.Fatalf("xray port %d did not open in time", port)
|
|
}
|