diff --git a/handler/socks/v5/udp.go b/handler/socks/v5/udp.go index f90decdc..66bb2a38 100644 --- a/handler/socks/v5/udp.go +++ b/handler/socks/v5/udp.go @@ -93,6 +93,21 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st return err } + // A direct (no-chain) UDP association yields a raw *net.UDPConn, which + // cannot consume the domainAddr returned by udpConn.ReadFrom for + // ATYP=DOMAINNAME datagrams. Wrap it so domains are resolved through the + // configured resolver (hostMapper → resolver → system DNS) instead of the + // previous hardcoded net.ResolveUDPAddr in the SOCKS5 decode path. + // Chain-backed PacketConns (udpTunConn, udpRelayConn, ...) encode domains + // as ATYP=Domain themselves and are left untouched. + if _, isDirect := pc.(*net.UDPConn); isDirect { + pc = &resolvePacketConn{ + PacketConn: pc, + resolver: h.options.Router.Options().Resolver, + hostMapper: h.options.Router.Options().HostMapper, + } + } + saddr := gosocks5.Addr{} saddr.ParseFrom(cc.LocalAddr().String()) @@ -203,6 +218,55 @@ func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error } } +// resolvePacketConn wraps the upstream net.PacketConn for a direct (no-chain) +// UDP association and resolves domain addresses to IPs in WriteTo calls. +// +// udpConn.ReadFrom now returns a domainAddr for ATYP=DOMAINNAME datagrams so +// the domain survives to the chain; a raw *net.UDPConn cannot consume a +// domainAddr (WriteTo needs *net.UDPAddr), so direct connections are wrapped +// here. Resolution order is hostMapper → configured resolver → system DNS, so a +// configured resolver (e.g. resolver=1.1.1.1) is honored instead of leaking +// through the system resolver. +type resolvePacketConn struct { + net.PacketConn + resolver resolver.Resolver + hostMapper hosts.HostMapper +} + +func (c *resolvePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + host, portStr, err := net.SplitHostPort(addr.String()) + if err != nil { + return c.PacketConn.WriteTo(b, addr) + } + if net.ParseIP(host) != nil { + return c.PacketConn.WriteTo(b, addr) + } + + var ips []net.IP + if c.hostMapper != nil { + ips, _ = c.hostMapper.Lookup(context.Background(), "ip", host) + } + if len(ips) == 0 && c.resolver != nil { + ips, _ = c.resolver.Resolve(context.Background(), "ip", host) + } + if len(ips) == 0 { + ips, _ = net.LookupIP(host) + } + if len(ips) == 0 { + return 0, fmt.Errorf("socks5 udp: cannot resolve %s", host) + } + + ip := ips[0] + for _, candidate := range ips { + if candidate.To4() != nil { + ip = candidate + break + } + } + port, _ := strconv.Atoi(portStr) + return c.PacketConn.WriteTo(b, &net.UDPAddr{IP: ip, Port: port}) +} + // domainResolvePacketConn wraps a net.PacketConn and resolves domain // addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP // response datagrams never carry ATYP=Domain (0x03), which some clients diff --git a/internal/util/socks/conn.go b/internal/util/socks/conn.go index d0c888e7..1cce1342 100644 --- a/internal/util/socks/conn.go +++ b/internal/util/socks/conn.go @@ -3,6 +3,7 @@ package socks import ( "bytes" "net" + "strconv" "github.com/go-gost/core/common/bufpool" "github.com/go-gost/gosocks5" @@ -106,6 +107,20 @@ func UDPConn(c net.PacketConn, bufferSize int) net.PacketConn { } } +// domainAddr is a net.Addr that preserves a domain name without resolving it. +// Returned by udpConn.ReadFrom when a SOCKS5 UDP datagram carries +// ATYP=DOMAINNAME, so that downstream consumers (e.g. relay-chain WriteTo) can +// forward the domain verbatim instead of forcing a local DNS lookup that would +// leak the query through the system resolver and bypass any configured resolver. +type domainAddr struct { + network string + host string + port int +} + +func (a *domainAddr) Network() string { return a.network } +func (a *domainAddr) String() string { return net.JoinHostPort(a.host, strconv.Itoa(a.port)) } + // ReadFrom reads an UDP datagram. // NOTE: for server side, // the returned addr is the target address the client want to relay to. @@ -128,7 +143,11 @@ func (c *udpConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { } n = copy(b, buf[hlen:n]) - addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + if net.ParseIP(socksAddr.Host) != nil { + addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + } else { + addr = &domainAddr{network: "udp", host: socksAddr.Host, port: int(socksAddr.Port)} + } return }