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@ // 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, 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.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, 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) }