fix(net): defer DNS resolution to final dialer to preserve domain in proxy chain

Commit 0522ae8 added inline DNS resolution (PreferGo: true) in Resolve(),
resolving domain names to IPs before routing. This caused the SOCKS5
connector to send CONNECT with ATYP=IPv4 instead of ATYP=domain, breaking
chains where the backend (e.g., ssh -D) cannot reach the gost-resolved IP.

Revert the DNS fallback in Resolve() — return the address unchanged so
the original hostname flows through the proxy chain. Instead, add
net.Resolver{PreferGo: true} to the TCP dialer in internal/net/dialer,
which resolves DNS at the point of actual TCP connection — still pure
Go (no cgo thread exhaustion) but after the proxy chain routing is done.

Also rename resovle.go → resolve.go (fix typo).

Fixes go-gost/gost#877
This commit is contained in:
ginuerzh
2026-06-25 11:02:36 +08:00
parent ec4791485c
commit 8fd5200bf1
3 changed files with 21 additions and 25 deletions
+1
View File
@@ -146,6 +146,7 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i
return nil, fmt.Errorf("dial: unsupported network %s", network) return nil, fmt.Errorf("dial: unsupported network %s", network)
} }
netd := net.Dialer{ netd := net.Dialer{
Resolver: &net.Resolver{PreferGo: true},
LocalAddr: ifAddr, LocalAddr: ifAddr,
Control: func(network, address string, c syscall.RawConn) error { Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) { return c.Control(func(fd uintptr) {
@@ -13,8 +13,10 @@ import (
// Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is // Resolve resolves addr to a concrete IP address. If hosts is non-nil, it is
// consulted first. If r is non-nil and no host mapping matched, the resolver // consulted first. If r is non-nil and no host mapping matched, the resolver
// is used. If neither is available, Go's native pure-Go resolver is used as a // is used. If neither is available, the address is returned unchanged so that
// fallback to avoid cgo-based DNS lookups that would block OS threads. // the original hostname is preserved for downstream connectors (e.g., SOCKS5
// ATYP=domain). DNS resolution is deferred to the final TCP dialer, which uses
// a pure-Go resolver to avoid cgo thread exhaustion.
func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) { func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) {
if addr == "" { if addr == "" {
return addr, nil return addr, nil
@@ -53,22 +55,9 @@ func Resolve(ctx context.Context, network, addr string, r resolver.Resolver, hos
return net.JoinHostPort(ips[0].String(), port), nil return net.JoinHostPort(ips[0].String(), port), nil
} }
// A literal IP address needs no resolution. // Return the address unchanged so that the original hostname is
if net.ParseIP(host) != nil { // preserved through the proxy chain. DNS resolution happens in the
// final TCP dialer (dialer.go) which uses net.Resolver{PreferGo: true}
// to avoid cgo-based lookups that block OS threads.
return addr, nil return addr, nil
} }
// Fallback to Go's native (pure-Go) resolver when no custom resolver or
// host mapper is configured. This avoids cgo-based DNS lookups in
// downstream net.Dialer.DialContext which would block OS threads and
// cause thread exhaustion under high connection concurrency.
lr := &net.Resolver{PreferGo: true}
ips, err := lr.LookupIP(ctx, "ip", host)
if err != nil {
return "", err
}
if len(ips) == 0 {
return "", fmt.Errorf("resolver: domain %s does not exist", host)
}
return net.JoinHostPort(ips[0].String(), port), nil
}
@@ -115,11 +115,10 @@ func TestResolve_ResolverEmptyResult(t *testing.T) {
} }
func TestResolve_NoResolverNoHosts(t *testing.T) { func TestResolve_NoResolverNoHosts(t *testing.T) {
// When no resolver and no host mapper are configured, the function now // When no resolver and no host mapper are configured, the address is
// resolves via Go's native DNS resolver (PreferGo: true). This test // returned unchanged so that the original hostname is preserved
// uses an IP address which is short-circuited at the top of Resolve, // through the proxy chain (e.g., SOCKS5 ATYP=domain).
// avoiding the need for actual DNS resolution in unit tests. // DNS resolution is deferred to the final TCP dialer.
// DNS fallback behavior is verified by the e2e tests.
ctx := context.Background() ctx := context.Background()
addr, err := Resolve(ctx, "tcp", "1.2.3.4:8080", nil, nil, nil) addr, err := Resolve(ctx, "tcp", "1.2.3.4:8080", nil, nil, nil)
if err != nil { if err != nil {
@@ -128,6 +127,13 @@ func TestResolve_NoResolverNoHosts(t *testing.T) {
if addr != "1.2.3.4:8080" { if addr != "1.2.3.4:8080" {
t.Errorf("expected 1.2.3.4:8080, got %s", addr) t.Errorf("expected 1.2.3.4:8080, got %s", addr)
} }
addr, err = Resolve(ctx, "tcp", "example.com:8080", nil, nil, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if addr != "example.com:8080" {
t.Errorf("expected example.com:8080, got %s", addr)
}
} }
func TestResolve_HostsNoMatchUsesResolver(t *testing.T) { func TestResolve_HostsNoMatchUsesResolver(t *testing.T) {