mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
75f3702dd3
The panel's SOCKS5 egress for AmneziaWG outbounds bound the fixed 127.0.0.1:64900, and every generated socks bridge dialed that constant. 64900 sits inside Windows' dynamic port range, where the OS can reserve whole blocks (this host excludes 64885-64984), so on the Windows builds release.yml ships the listener could stay down and every AmneziaWG outbound with it. Listen now tries 64900 first and falls back to any free loopback port; bridges, the outbound probe and the port-conflict check use EgressPort(), the port actually held. Bridges are generated apart from the listener, so BuildSocksBridge records the port it wrote and the AmneziaWG job requests an Xray restart while the listener holds a different one. Where 64900 is free nothing changes. The job's restart request is two lines of wiring no test reaches; the staleness it acts on is pinned by TestBridgesStaleUntilRegeneratedForTheBoundPort.
742 lines
21 KiB
Go
742 lines
21 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
|
|
|
"github.com/amnezia-vpn/amneziawg-go/v3/device"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
|
|
)
|
|
|
|
func verboseLoggerForTest(prefix string) *device.Logger {
|
|
return device.NewLogger(device.LogLevelVerbose, prefix)
|
|
}
|
|
|
|
const (
|
|
tunnelTestClientAddr = "10.203.0.2"
|
|
tunnelTestServerAddr = "10.203.0.1"
|
|
tunnelTestClientAddrV6 = "fd00:203::2"
|
|
tunnelTestServerAddrV6 = "fd00:203::1"
|
|
egressTestDialTimeout = 5 * time.Second
|
|
)
|
|
|
|
// pairedTunnel wires an outbound client device to an embedded server device
|
|
// over host UDP; the server stack hosts the far-end services under test.
|
|
type pairedTunnel struct {
|
|
client *Device
|
|
server *Device
|
|
serverIP netip.Addr
|
|
}
|
|
|
|
func newPairedTunnelForTest(t *testing.T) *pairedTunnel {
|
|
t.Helper()
|
|
slog := verboseLoggerForTest("(tsrv) ")
|
|
serverPriv, serverPub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientPriv, clientPub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listenPort := pc.LocalAddr().(*net.UDPAddr).Port
|
|
pc.Close()
|
|
|
|
obf := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
|
|
serverInst := amneziawg.Instance{
|
|
Id: 1,
|
|
InterfaceName: "awg-dnstest",
|
|
ListenPort: listenPort,
|
|
PrivateKey: serverPriv,
|
|
PublicKey: serverPub,
|
|
Address: []string{tunnelTestServerAddr + "/24"},
|
|
MTU: 1420,
|
|
Obfuscation: obf,
|
|
Peers: []amneziawg.Peer{{
|
|
PublicKey: clientPub,
|
|
AllowedIPs: []string{tunnelTestClientAddr + "/32"},
|
|
}},
|
|
}
|
|
server, err := newUnconfiguredDevice(serverInst, DeviceOptions{Logger: slog})
|
|
if err != nil {
|
|
t.Fatalf("server device: %v", err)
|
|
}
|
|
t.Cleanup(server.Close)
|
|
|
|
// Server Up before client exists: the first handshake fires at
|
|
// ConfigureClient; a missed initiation costs a 5s REKEY_TIMEOUT.
|
|
if err := server.Configure(serverInst, DeviceOptions{Logger: slog}); err != nil {
|
|
t.Fatalf("server Configure: %v", err)
|
|
}
|
|
|
|
clientInst := amneziawg.OutboundInstance{
|
|
Tag: "awg-dom-test",
|
|
Address: []string{tunnelTestClientAddr + "/32"},
|
|
MTU: 1420,
|
|
PrivateKey: clientPriv,
|
|
Obfuscation: obf,
|
|
Peers: []amneziawg.OutboundPeer{{
|
|
PublicKey: serverPub,
|
|
Endpoint: net.JoinHostPort("127.0.0.1", strconv.Itoa(listenPort)),
|
|
AllowedIPs: []string{"0.0.0.0/0", "::/0"},
|
|
KeepAlive: 1,
|
|
}},
|
|
}
|
|
clog := verboseLoggerForTest("(tcli) ")
|
|
client, err := newUnconfiguredClientDevice(clientInst, DeviceOptions{Logger: clog})
|
|
if err != nil {
|
|
t.Fatalf("client device: %v", err)
|
|
}
|
|
if err := client.ConfigureClient(clientInst, DeviceOptions{Logger: clog}); err != nil {
|
|
client.Close()
|
|
t.Fatalf("ConfigureClient: %v", err)
|
|
}
|
|
t.Cleanup(client.Close)
|
|
|
|
return &pairedTunnel{
|
|
client: client,
|
|
server: server,
|
|
serverIP: netip.MustParseAddr(tunnelTestServerAddr),
|
|
}
|
|
}
|
|
|
|
func newPairedTunnelV6ForTest(t *testing.T) *pairedTunnel {
|
|
t.Helper()
|
|
slog := verboseLoggerForTest("(tsrv6) ")
|
|
serverPriv, serverPub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientPriv, clientPub, err := wgutil.GenerateWireguardKeypair()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listenPort := pc.LocalAddr().(*net.UDPAddr).Port
|
|
pc.Close()
|
|
|
|
obf := amneziawg.Obfuscation31{Jc: 4, Jmin: 40, Jmax: 70, S1: 20, S2: 30, S3: 20, S4: 20}
|
|
serverInst := amneziawg.Instance{
|
|
Id: 2,
|
|
InterfaceName: "awg-dnstest6",
|
|
ListenPort: listenPort,
|
|
PrivateKey: serverPriv,
|
|
PublicKey: serverPub,
|
|
Address: []string{tunnelTestServerAddrV6 + "/64", "2606:4700:4700::1111/128"},
|
|
MTU: 1420,
|
|
Obfuscation: obf,
|
|
Peers: []amneziawg.Peer{{
|
|
PublicKey: clientPub,
|
|
AllowedIPs: []string{tunnelTestClientAddrV6 + "/128"},
|
|
}},
|
|
}
|
|
server, err := newUnconfiguredDevice(serverInst, DeviceOptions{Logger: slog})
|
|
if err != nil {
|
|
t.Fatalf("server device: %v", err)
|
|
}
|
|
t.Cleanup(server.Close)
|
|
|
|
if err := server.Configure(serverInst, DeviceOptions{Logger: slog}); err != nil {
|
|
t.Fatalf("server Configure: %v", err)
|
|
}
|
|
|
|
clientInst := amneziawg.OutboundInstance{
|
|
Tag: "awg-dom-v6-test",
|
|
Address: []string{tunnelTestClientAddrV6 + "/128"},
|
|
MTU: 1420,
|
|
PrivateKey: clientPriv,
|
|
Obfuscation: obf,
|
|
Peers: []amneziawg.OutboundPeer{{
|
|
PublicKey: serverPub,
|
|
Endpoint: net.JoinHostPort("127.0.0.1", strconv.Itoa(listenPort)),
|
|
AllowedIPs: []string{"::/0"},
|
|
KeepAlive: 1,
|
|
}},
|
|
}
|
|
clog := verboseLoggerForTest("(tcli6) ")
|
|
client, err := newUnconfiguredClientDevice(clientInst, DeviceOptions{Logger: clog})
|
|
if err != nil {
|
|
t.Fatalf("client device: %v", err)
|
|
}
|
|
if err := client.ConfigureClient(clientInst, DeviceOptions{Logger: clog}); err != nil {
|
|
client.Close()
|
|
t.Fatalf("ConfigureClient: %v", err)
|
|
}
|
|
t.Cleanup(client.Close)
|
|
|
|
return &pairedTunnel{
|
|
client: client,
|
|
server: server,
|
|
serverIP: netip.MustParseAddr(tunnelTestServerAddrV6),
|
|
}
|
|
}
|
|
|
|
func registerEgressDeviceForTest(t *testing.T, dev *Device) {
|
|
t.Helper()
|
|
srv := GetEgressServer()
|
|
srv.SetStack("awg-dom-test", dev)
|
|
if err := srv.Listen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { srv.DeleteStack("awg-dom-test") })
|
|
}
|
|
|
|
// startTunnelDNS answers A/AAAA queries from INSIDE the server's netstack;
|
|
// reaching it proves DNS rode the tunnel, not the host resolver.
|
|
func (p *pairedTunnel) startDNS(t *testing.T, answer netip.Addr) chan string {
|
|
t.Helper()
|
|
proto := ipv4.ProtocolNumber
|
|
if p.serverIP.Is6() {
|
|
proto = ipv6.ProtocolNumber
|
|
}
|
|
ln, err := gonet.DialUDP(p.server.Stack, &tcpip.FullAddress{NIC: 1, Port: 53}, nil, proto)
|
|
if err != nil {
|
|
t.Fatalf("bind fake dns in server stack: %v", err)
|
|
}
|
|
got := make(chan string, 8)
|
|
go func() {
|
|
defer ln.Close()
|
|
buf := make([]byte, 512)
|
|
for {
|
|
n, from, rerr := ln.ReadFrom(buf)
|
|
if rerr != nil {
|
|
return
|
|
}
|
|
q := buf[:n]
|
|
if name := dnsQuestionName(q); name != "" {
|
|
select {
|
|
case got <- name:
|
|
default:
|
|
}
|
|
}
|
|
if resp := buildARecordReply(q, answer); resp != nil {
|
|
if _, werr := ln.WriteTo(resp, from); werr != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
t.Cleanup(func() { ln.Close() })
|
|
return got
|
|
}
|
|
|
|
func (p *pairedTunnel) overrideDNS(t *testing.T, answer netip.Addr) chan string {
|
|
t.Helper()
|
|
srv := GetEgressServer()
|
|
prev := srv.currentDNSServer()
|
|
srv.SetDNSServer(net.JoinHostPort(p.serverIP.String(), "53"))
|
|
t.Cleanup(func() { srv.SetDNSServer(prev) })
|
|
resetTunnelDNSCacheForTest()
|
|
return p.startDNS(t, answer)
|
|
}
|
|
|
|
func resetTunnelDNSCacheForTest() {
|
|
tunnelDNSCache.mu.Lock()
|
|
tunnelDNSCache.m = map[string]tunnelDNSCacheEntry{}
|
|
tunnelDNSCache.mu.Unlock()
|
|
}
|
|
|
|
func dnsQuestionName(q []byte) string {
|
|
if len(q) < 12 {
|
|
return ""
|
|
}
|
|
i := 12
|
|
var parts []byte
|
|
for i < len(q) {
|
|
l := int(q[i])
|
|
i++
|
|
if l == 0 {
|
|
break
|
|
}
|
|
if i+l > len(q) || l > 63 {
|
|
return ""
|
|
}
|
|
parts = append(parts, q[i:i+l]...)
|
|
parts = append(parts, '.')
|
|
i += l
|
|
}
|
|
for len(parts) > 0 && parts[len(parts)-1] == '.' {
|
|
parts = parts[:len(parts)-1]
|
|
}
|
|
return string(parts)
|
|
}
|
|
|
|
func buildARecordReply(q []byte, answer netip.Addr) []byte {
|
|
if len(q) < 17 {
|
|
return nil
|
|
}
|
|
out := make([]byte, 0, len(q)+16)
|
|
header := make([]byte, 12)
|
|
copy(header[0:2], q[0:2])
|
|
header[2] = 0x81 // QR=1 RD=1
|
|
header[3] = 0x80 // RA=1 RCODE=0
|
|
binary.BigEndian.PutUint16(header[4:], 1)
|
|
binary.BigEndian.PutUint16(header[6:], 1)
|
|
out = append(out, header...)
|
|
end := len(q)
|
|
for end >= 5 && q[end-4] == 0 && q[end-3] == 0 && q[end-2] == 0 && q[end-1] == 0 {
|
|
end -= 4
|
|
}
|
|
out = append(out, q[12:end]...)
|
|
if answer.Is4() {
|
|
a := answer.As4()
|
|
rr := make([]byte, 16)
|
|
rr[0], rr[1] = 0xc0, 0x0c
|
|
binary.BigEndian.PutUint16(rr[2:], 1) // Type A
|
|
binary.BigEndian.PutUint16(rr[4:], 1) // IN
|
|
binary.BigEndian.PutUint32(rr[6:], 30) // TTL
|
|
binary.BigEndian.PutUint16(rr[10:], 4)
|
|
copy(rr[12:], a[:])
|
|
out = append(out, rr...)
|
|
} else if answer.Is6() {
|
|
a16 := answer.As16()
|
|
rr := make([]byte, 28)
|
|
rr[0], rr[1] = 0xc0, 0x0c
|
|
binary.BigEndian.PutUint16(rr[2:], 28) // Type AAAA
|
|
binary.BigEndian.PutUint16(rr[4:], 1) // IN
|
|
binary.BigEndian.PutUint32(rr[6:], 30) // TTL
|
|
binary.BigEndian.PutUint16(rr[10:], 16)
|
|
copy(rr[12:], a16[:])
|
|
out = append(out, rr...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func socksAuthUser(t *testing.T, ctl net.Conn, user string) {
|
|
t.Helper()
|
|
ctl.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
if _, err := ctl.Write([]byte{0x05, 0x02, 0x00, 0x02}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := make([]byte, 2)
|
|
if _, err := io.ReadFull(ctl, r); err != nil {
|
|
t.Fatalf("greeting read: %v", err)
|
|
}
|
|
pass := SocksPassword()
|
|
req := make([]byte, 0, 3+len(user)+len(pass))
|
|
req = append(req, 0x01, byte(len(user)))
|
|
req = append(req, user...)
|
|
req = append(req, byte(len(pass)))
|
|
req = append(req, pass...)
|
|
if _, err := ctl.Write(req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
auth := make([]byte, 2)
|
|
if _, err := io.ReadFull(ctl, auth); err != nil || auth[1] != 0x00 {
|
|
t.Fatalf("auth rejected: %v %v", err, auth)
|
|
}
|
|
}
|
|
|
|
func socksAuth(t *testing.T, ctl net.Conn) {
|
|
t.Helper()
|
|
socksAuthUser(t, ctl, "awg-dom-test")
|
|
}
|
|
|
|
func TestEgressGreetingRejectsNoAuthClient(t *testing.T) {
|
|
tun := newPairedTunnelForTest(t)
|
|
registerEgressDeviceForTest(t, tun.client)
|
|
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(GetEgressServer().Port())))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ctl.Close()
|
|
ctl.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
// Client offers only NO-AUTH; server must answer 0xFF.
|
|
if _, err := ctl.Write([]byte{0x05, 0x01, 0x00}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := make([]byte, 2)
|
|
if _, err := io.ReadFull(ctl, r); err != nil {
|
|
t.Fatalf("greeting read: %v", err)
|
|
}
|
|
if r[0] != 0x05 || r[1] != 0xFF {
|
|
t.Fatalf("greeting reply = %v, want 05 FF (auth required)", r)
|
|
}
|
|
}
|
|
|
|
func TestEgressConnectDomainResolvesThroughTunnel(t *testing.T) {
|
|
tun := newPairedTunnelForTest(t)
|
|
registerEgressDeviceForTest(t, tun.client)
|
|
// Resolving to the server's own tunnel address makes the follow-up dial
|
|
// fail fast (nothing listens on :80), while proving resolution happened.
|
|
gotQuery := tun.overrideDNS(t, tun.serverIP)
|
|
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(GetEgressServer().Port())))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ctl.Close()
|
|
|
|
socksAuth(t, ctl)
|
|
name := "example.internal"
|
|
req := make([]byte, 0, 7+len(name))
|
|
req = append(req, 0x05, 0x01, 0x00, 0x03, byte(len(name)))
|
|
req = append(req, name...)
|
|
req = append(req, 0x00, 0x50)
|
|
if _, err := ctl.Write(req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case queried := <-gotQuery:
|
|
if len(queried) < len(name) || queried[:len(name)] != name {
|
|
t.Fatalf("resolver queried %q, want prefix %q -- DNS did not ride the tunnel", queried, name)
|
|
}
|
|
case <-time.After(egressTestDialTimeout):
|
|
t.Fatal("no DNS query reached the in-tunnel resolver")
|
|
}
|
|
|
|
reply := make([]byte, 10)
|
|
ctl.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
if _, err := io.ReadFull(ctl, reply); err != nil {
|
|
t.Fatalf("read reply: %v", err)
|
|
}
|
|
if reply[1] == 0x00 {
|
|
t.Fatal("unexpected success: nothing should be listening on the resolved address")
|
|
}
|
|
}
|
|
|
|
func TestEgressConnectDomainIPv6OnlyTunnelResolvesThroughTunnel(t *testing.T) {
|
|
tun := newPairedTunnelV6ForTest(t)
|
|
srv := GetEgressServer()
|
|
srv.SetStack("awg-dom-v6-test", tun.client)
|
|
if err := srv.Listen(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { srv.DeleteStack("awg-dom-v6-test") })
|
|
|
|
// No override: a blank dns has to fall through currentDNSServer to
|
|
// defaultDNSFor, which the server stack answers on its own v6 /128.
|
|
prevDNS := srv.currentDNSServer()
|
|
srv.SetDNSServer("")
|
|
t.Cleanup(func() { srv.SetDNSServer(prevDNS) })
|
|
resetTunnelDNSCacheForTest()
|
|
gotQuery := tun.startDNS(t, tun.serverIP)
|
|
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(GetEgressServer().Port())))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ctl.Close()
|
|
|
|
socksAuthUser(t, ctl, "awg-dom-v6-test")
|
|
name := "v6.example.internal"
|
|
req := make([]byte, 0, 7+len(name))
|
|
req = append(req, 0x05, 0x01, 0x00, 0x03, byte(len(name)))
|
|
req = append(req, name...)
|
|
req = append(req, 0x00, 0x50)
|
|
if _, err := ctl.Write(req); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case queried := <-gotQuery:
|
|
if len(queried) < len(name) || queried[:len(name)] != name {
|
|
t.Fatalf("resolver queried %q, want prefix %q -- DNS did not ride the v6 tunnel", queried, name)
|
|
}
|
|
case <-time.After(egressTestDialTimeout):
|
|
t.Fatal("no DNS query reached the in-tunnel v6 resolver")
|
|
}
|
|
|
|
reply := make([]byte, 10)
|
|
ctl.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
if _, err := io.ReadFull(ctl, reply); err != nil {
|
|
t.Fatalf("read reply: %v", err)
|
|
}
|
|
if reply[1] == 0x00 {
|
|
t.Fatal("unexpected success: nothing should be listening on the resolved address")
|
|
}
|
|
}
|
|
|
|
func TestEgressUDPDatagramDomainForwardedIntoTunnel(t *testing.T) {
|
|
tun := newPairedTunnelForTest(t)
|
|
registerEgressDeviceForTest(t, tun.client)
|
|
gotQuery := tun.overrideDNS(t, tun.serverIP)
|
|
|
|
in, err := gonet.DialUDP(tun.server.Stack, &tcpip.FullAddress{NIC: 1, Port: 9999}, nil, ipv4.ProtocolNumber)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(GetEgressServer().Port())))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ctl.Close()
|
|
|
|
socksAuth(t, ctl)
|
|
if _, err := ctl.Write([]byte{0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reply := make([]byte, 10)
|
|
if _, err := io.ReadFull(ctl, reply); err != nil || reply[1] != 0x00 {
|
|
t.Fatalf("associate failed: %v %v", err, reply)
|
|
}
|
|
bindPort := binary.BigEndian.Uint16(reply[8:10])
|
|
|
|
udp, err := net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(bindPort)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer udp.Close()
|
|
udp.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
|
|
// Plain-IP control datagram isolates domain parsing from transport.
|
|
// Retried to avoid warmup race on slow -race runners.
|
|
ctrl := []byte{0x00, 0x00, 0x00, 0x01, 10, 203, 0, 1, 0x27, 0x0f, 'c', 't', 'r', 'l'}
|
|
rcv := make([]byte, 64)
|
|
var nr int
|
|
var rerr error
|
|
for attempt := 0; attempt < 3; attempt++ {
|
|
if _, err := udp.Write(ctrl); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
nr, _, rerr = in.ReadFrom(rcv)
|
|
if rerr == nil {
|
|
break
|
|
}
|
|
}
|
|
if rerr != nil {
|
|
t.Fatalf("CONTROL datagram never reached the tunnel target: %v", rerr)
|
|
}
|
|
if string(rcv[:nr]) != "ctrl" {
|
|
t.Fatalf("control payload = %q", rcv[:nr])
|
|
}
|
|
|
|
name := "quic.internal"
|
|
dgram := make([]byte, 0, 5+len(name)+2+4)
|
|
dgram = append(dgram, 0x00, 0x00, 0x00, 0x03, byte(len(name)))
|
|
dgram = append(dgram, name...)
|
|
dgram = append(dgram, 0x27, 0x0f)
|
|
dgram = append(dgram, 'p', 'i', 'n', 'g')
|
|
|
|
var queried string
|
|
for attempt := 0; attempt < 3 && queried == ""; attempt++ {
|
|
if _, err := udp.Write(dgram); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case q := <-gotQuery:
|
|
queried = q
|
|
case <-time.After(1500 * time.Millisecond):
|
|
}
|
|
}
|
|
if len(queried) < len(name) || queried[:len(name)] != name {
|
|
t.Fatalf("resolver queried %q, want prefix %q -- DNS did not ride the tunnel", queried, name)
|
|
}
|
|
|
|
in.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
nr, _, rerr = in.ReadFrom(rcv)
|
|
if rerr != nil {
|
|
t.Fatalf("domain datagram never reached the tunnel target: %v", rerr)
|
|
}
|
|
if nr < 4 || string(rcv[:4]) != "ping" {
|
|
t.Fatalf("payload = %q (n=%d)", rcv[:nr], nr)
|
|
}
|
|
}
|
|
|
|
// TestEgressUDPDatagramDomainInterleavedClients ensures datagrams pass client
|
|
// address by value into resolver goroutines so responses route correctly.
|
|
func TestEgressUDPDatagramDomainInterleavedClients(t *testing.T) {
|
|
tun := newPairedTunnelForTest(t)
|
|
registerEgressDeviceForTest(t, tun.client)
|
|
gotQuery := tun.overrideDNS(t, tun.serverIP)
|
|
|
|
in, err := gonet.DialUDP(tun.server.Stack, &tcpip.FullAddress{NIC: 1, Port: 9999}, nil, ipv4.ProtocolNumber)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer in.Close()
|
|
|
|
go func() {
|
|
buf := make([]byte, 512)
|
|
for {
|
|
n, from, rerr := in.ReadFrom(buf)
|
|
if rerr != nil {
|
|
return
|
|
}
|
|
_, _ = in.WriteTo(append([]byte("echo:"), buf[:n]...), from)
|
|
}
|
|
}()
|
|
|
|
dialUDP := func() *net.UDPConn {
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(GetEgressServer().Port())))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { ctl.Close() })
|
|
socksAuth(t, ctl)
|
|
if _, err := ctl.Write([]byte{0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reply := make([]byte, 10)
|
|
if _, err := io.ReadFull(ctl, reply); err != nil || reply[1] != 0x00 {
|
|
t.Fatalf("associate failed: %v %v", err, reply)
|
|
}
|
|
bindPort := binary.BigEndian.Uint16(reply[8:10])
|
|
udp, err := net.DialUDP("udp", nil, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: int(bindPort)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { udp.Close() })
|
|
udp.SetDeadline(time.Now().Add(egressTestDialTimeout))
|
|
return udp
|
|
}
|
|
|
|
name := func(i int) string { return fmt.Sprintf("interleaved-%d.internal", i) }
|
|
dgram := func(i int, payload string) []byte {
|
|
n := name(i)
|
|
d := make([]byte, 0, 5+len(n)+2+len(payload))
|
|
d = append(d, 0x00, 0x00, 0x00, 0x03, byte(len(n)))
|
|
d = append(d, n...)
|
|
d = append(d, 0x27, 0x0f)
|
|
return append(d, payload...)
|
|
}
|
|
|
|
for seq := 0; seq < 4; seq++ {
|
|
udp := dialUDP()
|
|
payload := fmt.Sprintf("p-%d", seq)
|
|
if _, err := udp.Write(dgram(seq, payload)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case q := <-gotQuery:
|
|
if !strings.HasPrefix(q, "interleaved-") {
|
|
t.Fatalf("resolver queried %q, want an interleaved-* name", q)
|
|
}
|
|
case <-time.After(4 * time.Second):
|
|
t.Fatalf("query %d not observed", seq)
|
|
}
|
|
rcv := make([]byte, 512)
|
|
nr, _, rerr := udp.ReadFrom(rcv)
|
|
if rerr != nil {
|
|
t.Fatalf("reply %d never reached client: %v", seq, rerr)
|
|
}
|
|
if nr < 10 || !strings.Contains(string(rcv[:nr]), "echo:"+payload) {
|
|
t.Fatalf("reply payload = %q, want echo:%s", rcv[:nr], payload)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultDNSFor(t *testing.T) {
|
|
v4 := netip.MustParseAddr("10.8.0.2")
|
|
v6 := netip.MustParseAddr("2001:db8::2")
|
|
|
|
if got := defaultDNSFor([]netip.Addr{v4}); got != DefaultTunnelDNSServer {
|
|
t.Errorf("defaultDNSFor(v4) = %q, want %q", got, DefaultTunnelDNSServer)
|
|
}
|
|
if got := defaultDNSFor([]netip.Addr{v4, v6}); got != DefaultTunnelDNSServer {
|
|
t.Errorf("defaultDNSFor(dual) = %q, want %q", got, DefaultTunnelDNSServer)
|
|
}
|
|
if got := defaultDNSFor([]netip.Addr{v6}); got != DefaultTunnelDNSServerV6 {
|
|
t.Errorf("defaultDNSFor(v6-only) = %q, want %q", got, DefaultTunnelDNSServerV6)
|
|
}
|
|
if got := defaultDNSFor(nil); got != DefaultTunnelDNSServer {
|
|
t.Errorf("defaultDNSFor(nil) = %q, want %q", got, DefaultTunnelDNSServer)
|
|
}
|
|
}
|
|
|
|
func TestParseDatagramDomainHeader(t *testing.T) {
|
|
hdr := []byte{0, 0, 0, 0x03, 4, 'a', 'b', '.', 'd', 0x00, 0x35, 'x'}
|
|
name, port, hdrLen, err := parseDatagramDomainHeader(hdr)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if name != "ab.d" || port != 53 || hdrLen != 11 {
|
|
t.Fatalf("name=%q port=%d hdrLen=%d", name, port, hdrLen)
|
|
}
|
|
truncated := []byte{0, 0, 0, 0x03, 200, 'a'}
|
|
if _, _, _, err := parseDatagramDomainHeader(truncated); err == nil {
|
|
t.Fatal("truncated domain accepted")
|
|
}
|
|
empty := []byte{0, 0, 0, 0x03, 0, 0x00, 0x35}
|
|
if _, _, _, err := parseDatagramDomainHeader(empty); err == nil {
|
|
t.Fatal("empty domain accepted")
|
|
}
|
|
}
|
|
|
|
func TestReadSocksRequestTargetKeepsHostnameUnresolved(t *testing.T) {
|
|
payload := append([]byte{byte(len("invalid."))}, []byte("invalid.")...)
|
|
payload = append(payload, 0x01, 0xbb)
|
|
tr, err := readSocksRequestTarget(bytes.NewReader(payload), 0x03)
|
|
if err != nil {
|
|
t.Fatalf("domain request rejected: %v", err)
|
|
}
|
|
if tr.host != "invalid." || tr.port != 443 || tr.ip.IsValid() {
|
|
t.Fatalf("target = %+v", tr)
|
|
}
|
|
}
|
|
|
|
func TestTunnelDNSCache_ScopedPerTagAndServer(t *testing.T) {
|
|
resetTunnelDNSCacheForTest()
|
|
tagA, tagB := "out-a", "out-b"
|
|
dns1, dns2 := "1.1.1.1:53", "8.8.8.8:53"
|
|
host := "example.com"
|
|
|
|
addrA := netip.MustParseAddr("10.0.0.1")
|
|
addrB := netip.MustParseAddr("10.0.0.2")
|
|
|
|
keyA := dnsCacheKey(tagA, dns1, host)
|
|
keyB := dnsCacheKey(tagB, dns1, host)
|
|
keyA2 := dnsCacheKey(tagA, dns2, host)
|
|
|
|
tunnelDNSCache.mu.Lock()
|
|
tunnelDNSCache.m[keyA] = tunnelDNSCacheEntry{addr: addrA, exp: time.Now().Add(time.Hour)}
|
|
tunnelDNSCache.m[keyB] = tunnelDNSCacheEntry{addr: addrB, exp: time.Now().Add(time.Hour)}
|
|
tunnelDNSCache.mu.Unlock()
|
|
|
|
tunnelDNSCache.mu.Lock()
|
|
eA, okA := tunnelDNSCache.m[keyA]
|
|
eB, okB := tunnelDNSCache.m[keyB]
|
|
_, okA2 := tunnelDNSCache.m[keyA2]
|
|
tunnelDNSCache.mu.Unlock()
|
|
|
|
if !okA || eA.addr != addrA {
|
|
t.Fatalf("tagA cache entry mismatch: %v, %v", okA, eA)
|
|
}
|
|
if !okB || eB.addr != addrB {
|
|
t.Fatalf("tagB cache entry mismatch: %v, %v", okB, eB)
|
|
}
|
|
if okA2 {
|
|
t.Fatal("key with different DNS server should not match")
|
|
}
|
|
|
|
flushTunnelDNSCacheForTag(tagA)
|
|
tunnelDNSCache.mu.Lock()
|
|
_, okAAfter := tunnelDNSCache.m[keyA]
|
|
_, okBAfter := tunnelDNSCache.m[keyB]
|
|
tunnelDNSCache.mu.Unlock()
|
|
|
|
if okAAfter {
|
|
t.Fatal("tagA entry should be flushed")
|
|
}
|
|
if !okBAfter {
|
|
t.Fatal("tagB entry should survive flush of tagA")
|
|
}
|
|
}
|