mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +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.
262 lines
7.7 KiB
Go
262 lines
7.7 KiB
Go
package amneziawgnet
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/netip"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/dns/dnsmessage"
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
)
|
|
|
|
// DefaultTunnelDNSServer resolves domain targets through outbound netstack.
|
|
const (
|
|
DefaultTunnelDNSServer = "1.1.1.1:53"
|
|
DefaultTunnelDNSServerV6 = "[2606:4700:4700::1111]:53"
|
|
)
|
|
|
|
func deviceHasV4(addrs []netip.Addr) bool {
|
|
for _, a := range addrs {
|
|
if a.Is4() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func deviceHasV6(addrs []netip.Addr) bool {
|
|
for _, a := range addrs {
|
|
if a.Is6() && !a.Is4In6() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// defaultDNSFor picks a resolver matching the tunnel address family:
|
|
// IPv4 default (or empty), or IPv6 default when IPv6-only.
|
|
func defaultDNSFor(addrs []netip.Addr) string {
|
|
if deviceHasV4(addrs) || len(addrs) == 0 {
|
|
return DefaultTunnelDNSServer
|
|
}
|
|
return DefaultTunnelDNSServerV6
|
|
}
|
|
|
|
const (
|
|
// tunnelResolveTimeout bounds one lookup inside a live connection handler.
|
|
tunnelResolveTimeout = 4 * time.Second
|
|
tunnelDNSPacketTimeout = 1200 * time.Millisecond
|
|
tunnelDNSAttempts = 3
|
|
)
|
|
|
|
type tunnelDNSCacheEntry struct {
|
|
addr netip.Addr
|
|
exp time.Time
|
|
}
|
|
|
|
var tunnelDNSCache = struct {
|
|
mu sync.Mutex
|
|
m map[string]tunnelDNSCacheEntry
|
|
}{m: map[string]tunnelDNSCacheEntry{}}
|
|
|
|
const (
|
|
tunnelDNSCacheTTL = 60 * time.Second
|
|
tunnelDNSCacheMaxSize = 1024
|
|
tunnelDNSMaxInFlight = 1024
|
|
)
|
|
|
|
// tunnelDNSSlots caps lookups in flight across all outbounds: each one holds a
|
|
// netstack port until the resolver answers or the lookup times out.
|
|
var tunnelDNSSlots = make(chan struct{}, tunnelDNSMaxInFlight)
|
|
|
|
// dnsCacheKey computes cache key scoped by outbound tag, server, and host.
|
|
func dnsCacheKey(tag, dnsServer, host string) string {
|
|
return tag + "|" + dnsServer + "|" + host
|
|
}
|
|
|
|
// tunnelDNSServerFor normalizes dnsServer, empty meaning the family default.
|
|
func tunnelDNSServerFor(dev *Device, dnsServer string) string {
|
|
if normDNS := amneziawg.NormalizeDNSServer(dnsServer); normDNS != "" {
|
|
return normDNS
|
|
}
|
|
return defaultDNSFor(dev.LocalAddresses())
|
|
}
|
|
|
|
// cachedTunnelAddr returns host's unexpired cache entry; it never queries.
|
|
func cachedTunnelAddr(dev *Device, tag string, dnsServer string, host string) (netip.Addr, bool) {
|
|
key := dnsCacheKey(tag, tunnelDNSServerFor(dev, dnsServer), host)
|
|
tunnelDNSCache.mu.Lock()
|
|
defer tunnelDNSCache.mu.Unlock()
|
|
e, ok := tunnelDNSCache.m[key]
|
|
return e.addr, ok && time.Now().Before(e.exp)
|
|
}
|
|
|
|
func resolveTunnelVia(ctx context.Context, dev *Device, tag string, dnsServer string, host string) (netip.Addr, error) {
|
|
if addr, ok := cachedTunnelAddr(dev, tag, dnsServer, host); ok {
|
|
return addr, nil
|
|
}
|
|
normDNS := tunnelDNSServerFor(dev, dnsServer)
|
|
key := dnsCacheKey(tag, normDNS, host)
|
|
now := time.Now()
|
|
|
|
select {
|
|
case tunnelDNSSlots <- struct{}{}:
|
|
defer func() { <-tunnelDNSSlots }()
|
|
case <-ctx.Done():
|
|
return netip.Addr{}, fmt.Errorf("dns lookup %q: too many lookups in flight: %w", host, ctx.Err())
|
|
}
|
|
|
|
server, err := netip.ParseAddrPort(normDNS)
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("bad tunnel DNS server %q: %w", normDNS, err)
|
|
}
|
|
raddr := tcpip.FullAddress{
|
|
NIC: 1,
|
|
Addr: tcpip.AddrFromSlice(server.Addr().AsSlice()),
|
|
Port: server.Port(),
|
|
}
|
|
conn, derr := gonet.DialUDP(dev.Stack, nil, &raddr, tunnelNetwork(server.Addr()))
|
|
if derr != nil {
|
|
logger.Warningf("amneziawgnet: resolveTunnel tag=%q host=%q server=%s localAddrs=%v err=%v", tag, host, server, dev.LocalAddresses(), derr)
|
|
return netip.Addr{}, fmt.Errorf("dns dial %s: %w", server, derr)
|
|
}
|
|
defer conn.Close()
|
|
|
|
addr, rerr := exchangeTunnelDNSWithFallback(ctx, conn, dev.LocalAddresses(), host)
|
|
if rerr != nil {
|
|
return netip.Addr{}, rerr
|
|
}
|
|
|
|
tunnelDNSCache.mu.Lock()
|
|
if len(tunnelDNSCache.m) >= tunnelDNSCacheMaxSize {
|
|
tunnelDNSCache.m = map[string]tunnelDNSCacheEntry{}
|
|
}
|
|
tunnelDNSCache.m[key] = tunnelDNSCacheEntry{addr: addr, exp: now.Add(tunnelDNSCacheTTL)}
|
|
tunnelDNSCache.mu.Unlock()
|
|
logger.Debugf("amneziawgnet: resolved tag=%q %q -> %s via tunnel", tag, host, addr)
|
|
return addr, nil
|
|
}
|
|
|
|
// flushTunnelDNSCacheForTag purges all cached DNS entries for an outbound tag.
|
|
func flushTunnelDNSCacheForTag(tag string) {
|
|
tunnelDNSCache.mu.Lock()
|
|
defer tunnelDNSCache.mu.Unlock()
|
|
prefix := tag + "|"
|
|
for k := range tunnelDNSCache.m {
|
|
if strings.HasPrefix(k, prefix) {
|
|
delete(tunnelDNSCache.m, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
// dnsQueryTypesFor asks only for families the tunnel can dial, so a v4-only tunnel
|
|
// never caches an unroutable AAAA answer (#6570). No addresses keeps A then AAAA.
|
|
func dnsQueryTypesFor(addrs []netip.Addr) []dnsmessage.Type {
|
|
hasV4 := deviceHasV4(addrs)
|
|
hasV6 := deviceHasV6(addrs)
|
|
switch {
|
|
case hasV4 && hasV6:
|
|
return []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
|
|
case hasV6:
|
|
return []dnsmessage.Type{dnsmessage.TypeAAAA}
|
|
case hasV4:
|
|
return []dnsmessage.Type{dnsmessage.TypeA}
|
|
default:
|
|
return []dnsmessage.Type{dnsmessage.TypeA, dnsmessage.TypeAAAA}
|
|
}
|
|
}
|
|
|
|
// tunnelSupportsAddr reports whether the device stack has a local address in
|
|
// the same family as ip (IPv4-mapped IPv6 counts as IPv4).
|
|
func tunnelSupportsAddr(addrs []netip.Addr, ip netip.Addr) bool {
|
|
if !ip.IsValid() {
|
|
return false
|
|
}
|
|
if ip.Is4() || ip.Is4In6() {
|
|
return deviceHasV4(addrs)
|
|
}
|
|
return deviceHasV6(addrs)
|
|
}
|
|
|
|
// exchangeTunnelDNSWithFallback returns the first answer among the families the
|
|
// device stack can route.
|
|
func exchangeTunnelDNSWithFallback(ctx context.Context, conn *gonet.UDPConn, addrs []netip.Addr, host string) (netip.Addr, error) {
|
|
types := dnsQueryTypesFor(addrs)
|
|
|
|
var firstErr error
|
|
for _, qType := range types {
|
|
addr, err := exchangeTunnelDNSQuery(ctx, conn, host, qType)
|
|
if err == nil {
|
|
return addr, nil
|
|
}
|
|
if firstErr == nil {
|
|
firstErr = err
|
|
}
|
|
}
|
|
return netip.Addr{}, firstErr
|
|
}
|
|
|
|
func exchangeTunnelDNSQuery(ctx context.Context, conn *gonet.UDPConn, host string, qType dnsmessage.Type) (netip.Addr, error) {
|
|
name, err := dnsmessage.NewName(host + ".")
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("dns name %q: %w", host, err)
|
|
}
|
|
id := uint16(rand.Intn(1 << 16))
|
|
query := dnsmessage.Message{
|
|
Header: dnsmessage.Header{ID: id, RecursionDesired: true},
|
|
Questions: []dnsmessage.Question{{
|
|
Name: name,
|
|
Type: qType,
|
|
Class: dnsmessage.ClassINET,
|
|
}},
|
|
}
|
|
wire, err := query.Pack()
|
|
if err != nil {
|
|
return netip.Addr{}, fmt.Errorf("dns pack %q: %w", host, err)
|
|
}
|
|
|
|
buf := make([]byte, 512)
|
|
for attempt := 0; attempt < tunnelDNSAttempts; attempt++ {
|
|
select {
|
|
case <-ctx.Done():
|
|
return netip.Addr{}, ctx.Err()
|
|
default:
|
|
}
|
|
if _, werr := conn.Write(wire); werr != nil {
|
|
return netip.Addr{}, fmt.Errorf("dns send %q: %w", host, werr)
|
|
}
|
|
if derr := conn.SetReadDeadline(time.Now().Add(tunnelDNSPacketTimeout)); derr != nil {
|
|
return netip.Addr{}, fmt.Errorf("dns deadline %q: %w", host, derr)
|
|
}
|
|
for {
|
|
n, rerr := conn.Read(buf)
|
|
if rerr != nil {
|
|
break // per-attempt timeout -> next attempt
|
|
}
|
|
var resp dnsmessage.Message
|
|
if uerr := resp.Unpack(buf[:n]); uerr != nil || resp.ID != id {
|
|
continue
|
|
}
|
|
for _, ans := range resp.Answers {
|
|
if a, ok := ans.Body.(*dnsmessage.AResource); ok && qType == dnsmessage.TypeA {
|
|
return netip.AddrFrom4(a.A), nil
|
|
}
|
|
if aaaa, ok := ans.Body.(*dnsmessage.AAAAResource); ok && qType == dnsmessage.TypeAAAA {
|
|
return netip.AddrFrom16(aaaa.AAAA), nil
|
|
}
|
|
}
|
|
return netip.Addr{}, fmt.Errorf("dns %q (type %v): rcode=%d answers=%d", host, qType, resp.RCode, len(resp.Answers))
|
|
}
|
|
}
|
|
return netip.Addr{}, fmt.Errorf("dns lookup %q (type %v): no answer after %d attempts", host, qType, tunnelDNSAttempts)
|
|
}
|