mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-20 12:55:00 +03:00
805f94a00c
Windows Firewall prompted on every run of amneziawgnet.test.exe, since the tests opened AmneziaWG, outbound-client and port-forward sockets on all interfaces and go test rebuilds the binary under a fresh temp path, so a granted exception never sticks. Route every "all interfaces" bind through wildcardBindHost, which is empty in production (behaviour unchanged) and pinned to 127.0.0.1 by the package TestMain.
617 lines
19 KiB
Go
617 lines
19 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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.Obfuscation31{
|
|
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 := newUnconfiguredDevice(inst, DeviceOptions{})
|
|
if err != nil {
|
|
t.Fatalf("newUnconfiguredDevice: %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)
|
|
})
|
|
|
|
// Configure (IpcSet) must come after both attaches -- see
|
|
// newUnconfiguredDevice's doc comment.
|
|
if err := dev.Configure(inst, DeviceOptions{}); err != nil {
|
|
t.Fatalf("Configure: %v", err)
|
|
}
|
|
|
|
// --- 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, newListenBind(""), 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.Obfuscation31{
|
|
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, newListenBind(""), 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)
|
|
}
|