From 61eb3e2a3b572b6e5ec973b4904833e451e3fa54 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 21 Jun 2026 15:12:53 +0800 Subject: [PATCH] feat(socks5): add udp.resolveDomain option for clients incompatible with ATYP=Domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a wrapper that intercepts WriteTo on the client-facing SOCKS5 UDP relay and resolves domain addresses to IPs before the SOCKS5 header is encoded. Controlled via the (or ) handler metadata flag. When enabled, SOCKS5 UDP response datagrams always carry ATYP=IPv4 or ATYP=IPv6 — never ATYP=Domain (0x03) — making them compatible with clients like tun2proxy and Surge that cannot parse domain-typed addresses in UDP. The wrapper resolves domains through the router's infrastructure: host mapper → resolver → system DNS fallback. Unresolvable domains cause the datagram to be silently dropped rather than forwarded in a format the client rejects. Fixes go-gost/gost#434 --- handler/socks/v5/metadata.go | 2 + handler/socks/v5/udp.go | 77 +++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/handler/socks/v5/metadata.go b/handler/socks/v5/metadata.go index 59aa2fb0..08e2b599 100644 --- a/handler/socks/v5/metadata.go +++ b/handler/socks/v5/metadata.go @@ -26,6 +26,7 @@ type metadata struct { enableBind bool enableUDP bool udpBufferSize int + udpResolveDomain bool compatibilityMode bool hash string muxCfg *mux.Config @@ -58,6 +59,7 @@ func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) { h.md.enableBind = mdutil.GetBool(md, "bind") h.md.enableUDP = mdutil.GetBool(md, "udp") h.md.udpBufferSize = mdutil.GetInt(md, "udp.bufferSize", "udpBufferSize") + h.md.udpResolveDomain = mdutil.GetBool(md, "udp.resolveDomain", "udpResolveDomain") h.md.compatibilityMode = mdutil.GetBool(md, "comp") h.md.hash = mdutil.GetString(md, "hash") diff --git a/handler/socks/v5/udp.go b/handler/socks/v5/udp.go index 2f445a0c..2d66383a 100644 --- a/handler/socks/v5/udp.go +++ b/handler/socks/v5/udp.go @@ -6,11 +6,14 @@ import ( "errors" "io" "net" + "strconv" "time" + "github.com/go-gost/core/hosts" "github.com/go-gost/core/limiter" "github.com/go-gost/core/logger" "github.com/go-gost/core/observer/stats" + "github.com/go-gost/core/resolver" "github.com/go-gost/gosocks5" ctxvalue "github.com/go-gost/x/ctx" ictx "github.com/go-gost/x/internal/ctx" @@ -129,12 +132,23 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st } } - r := udp.NewRelay(socks.UDPConn( + pc1 := socks.UDPConn( &filteredPacketConn{ PacketConn: cc, filterIP: conn.RemoteAddr().(*net.TCPAddr).IP, }, - h.md.udpBufferSize), pc). + h.md.udpBufferSize) + + if h.md.udpResolveDomain { + pc1 = &domainResolvePacketConn{ + PacketConn: pc1, + resolver: h.options.Router.Options().Resolver, + hostMapper: h.options.Router.Options().HostMapper, + log: log, + } + } + + r := udp.NewRelay(pc1, pc). WithService(h.options.Service). WithBypass(h.options.Bypass). WithBufferSize(h.md.udpBufferSize). @@ -170,3 +184,62 @@ func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error } } } + +// 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 +// (e.g., tun2proxy, Surge) cannot parse. +type domainResolvePacketConn struct { + net.PacketConn + resolver resolver.Resolver + hostMapper hosts.HostMapper + log logger.Logger +} + +func (c *domainResolvePacketConn) 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 { + // Already an IP address — pass through unchanged. + return c.PacketConn.WriteTo(b, addr) + } + + // Resolve the domain using the router's infrastructure + // (host mapper → resolver → system fallback) so the SOCKS5 encoder + // will use AddrIPv4 or AddrIPv6 instead of AddrDomain. + var ips []net.IP + if c.hostMapper != nil { + ips, _ = c.hostMapper.Lookup(context.Background(), "ip", host) + } + if len(ips) == 0 && c.resolver != nil { + ips, err = c.resolver.Resolve(context.Background(), "ip", host) + if err != nil && c.log != nil { + c.log.Warnf("socks5 udp: resolve %s: %v", host, err) + } + } + if len(ips) == 0 { + ips, err = net.LookupIP(host) + } + if err != nil || len(ips) == 0 { + // Cannot resolve the domain — drop the datagram rather than + // forwarding an ATYP=Domain response the client cannot parse. + if c.log != nil { + c.log.Warnf("socks5 udp: dropping datagram to %s (DNS resolution failed)", host) + } + return 0, nil + } + + // Pick the first IPv4 address from the result set, falling back to + // the first entry when no IPv4 is available. + 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}) +}