mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
bc2c64d901
relayUDP started a goroutine for every domain-addressed datagram and each lookup opened its own netstack UDP endpoint, so the lookups in flight were bounded only by the client's send rate: one proxy user could grow the panel by gigabytes and exhaust the outbound stack's ports, stalling every user of that outbound. One association may now have 32 datagrams waiting on DNS and drops the rest, a cached name is delivered from the reader loop without a lookup, and resolveTunnelVia takes one of 1024 process-wide slots before it opens an endpoint, which also covers the TCP CONNECT path.
856 lines
25 KiB
Go
856 lines
25 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/netip"
|
|
"runtime"
|
|
"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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEgressUDPDatagramDomainFloodBoundsPendingLookups ensures datagrams to a
|
|
// name whose lookup never completes do not pin a goroutine and a port each.
|
|
func TestEgressUDPDatagramDomainFloodBoundsPendingLookups(t *testing.T) {
|
|
tun := newPairedTunnelForTest(t)
|
|
registerEgressDeviceForTest(t, tun.client)
|
|
srv := GetEgressServer()
|
|
prev := srv.currentDNSServer()
|
|
srv.SetDNSServer(net.JoinHostPort(tun.serverIP.String(), "53"))
|
|
t.Cleanup(func() { srv.SetDNSServer(prev) })
|
|
resetTunnelDNSCacheForTest()
|
|
|
|
silentDNS, err := gonet.DialUDP(tun.server.Stack, &tcpip.FullAddress{NIC: 1, Port: 53}, nil, ipv4.ProtocolNumber)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer silentDNS.Close()
|
|
|
|
ctl, err := (&net.Dialer{Timeout: egressTestDialTimeout}).Dial("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(srv.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()
|
|
|
|
name := "never-answers.internal"
|
|
dgram := make([]byte, 0, 5+len(name)+2+1)
|
|
dgram = append(dgram, 0x00, 0x00, 0x00, 0x03, byte(len(name)))
|
|
dgram = append(dgram, name...)
|
|
dgram = append(dgram, 0x27, 0x0f, 'x')
|
|
|
|
const flood = 2000
|
|
before := runtime.NumGoroutine()
|
|
for i := 0; i < flood; i++ {
|
|
if _, err := udp.Write(dgram); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if i%50 == 49 {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
if grown := runtime.NumGoroutine() - before; grown > maxPendingUDPResolves+8 {
|
|
t.Fatalf("%d datagrams left %d goroutines behind, want at most %d pending lookups", flood, grown, maxPendingUDPResolves)
|
|
}
|
|
|
|
// A cached name takes no lookup slot, so it still gets through.
|
|
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()
|
|
cached := "cached.internal"
|
|
tunnelDNSCache.mu.Lock()
|
|
tunnelDNSCache.m[dnsCacheKey("awg-dom-test", srv.currentDNSServer(), cached)] = tunnelDNSCacheEntry{addr: tun.serverIP, exp: time.Now().Add(time.Hour)}
|
|
tunnelDNSCache.mu.Unlock()
|
|
dgram = append([]byte{0x00, 0x00, 0x00, 0x03, byte(len(cached))}, cached...)
|
|
dgram = append(dgram, 0x27, 0x0f, 'p', 'i', 'n', 'g')
|
|
rcv := make([]byte, 64)
|
|
var nr int
|
|
var rerr error
|
|
for attempt := 0; attempt < 3; attempt++ {
|
|
if _, err := udp.Write(dgram); 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 || string(rcv[:nr]) != "ping" {
|
|
t.Fatalf("cached-name datagram behind the flood: payload = %q, err = %v", rcv[:nr], rerr)
|
|
}
|
|
}
|
|
|
|
// TestResolveTunnelViaWaitsForLookupSlot ensures a cache miss opens no tunnel
|
|
// endpoint (the nil Stack would panic) while every lookup slot is taken.
|
|
func TestResolveTunnelViaWaitsForLookupSlot(t *testing.T) {
|
|
resetTunnelDNSCacheForTest()
|
|
for i := 0; i < cap(tunnelDNSSlots); i++ {
|
|
tunnelDNSSlots <- struct{}{}
|
|
}
|
|
t.Cleanup(func() {
|
|
for i := 0; i < cap(tunnelDNSSlots); i++ {
|
|
<-tunnelDNSSlots
|
|
}
|
|
})
|
|
|
|
dev := &Device{localAddrs: []netip.Addr{netip.MustParseAddr(tunnelTestClientAddr)}}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
if _, err := resolveTunnelVia(ctx, dev, "awg-dom-test", "", "slots.internal"); !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("err = %v, want context.DeadlineExceeded", err)
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|