fix(net): use pure-Go DNS fallback to prevent thread exhaustion

When no custom resolver or host mapper is configured, xnet.Resolve now
resolves hostnames via Go's native pure-Go resolver (PreferGo: true)
instead of returning the hostname unchanged.

Previously, the unresolved hostname would reach net.Dialer.DialContext,
which on CGO_ENABLED=1 builds used cgo-based DNS — blocking a dedicated
OS thread per concurrent lookup. At scale (1000+ listeners), this
exhausted Go's 10000-thread limit.

Fixes go-gost/gost#550
This commit is contained in:
ginuerzh
2026-06-21 22:34:43 +08:00
parent 55a246869e
commit 0522ae8155
2 changed files with 29 additions and 5 deletions
+21 -2
View File
@@ -13,7 +13,8 @@ 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, addr is returned unchanged. // is used. If neither is available, Go's native pure-Go resolver is used as a
// fallback to avoid cgo-based DNS lookups that would block OS threads.
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
@@ -51,5 +52,23 @@ 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
} }
return addr, nil
// A literal IP address needs no resolution.
if net.ParseIP(host) != 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
} }
+8 -3
View File
@@ -115,13 +115,18 @@ 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
// resolves via Go's native DNS resolver (PreferGo: true). This test
// uses an IP address which is short-circuited at the top of Resolve,
// avoiding the need for actual DNS resolution in unit tests.
// DNS fallback behavior is verified by the e2e tests.
ctx := context.Background() ctx := context.Background()
addr, err := Resolve(ctx, "tcp", "example.com:8080", nil, nil, nil) addr, err := Resolve(ctx, "tcp", "1.2.3.4:8080", nil, nil, nil)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
if addr != "example.com:8080" { if addr != "1.2.3.4:8080" {
t.Errorf("expected example.com:8080, got %s", addr) t.Errorf("expected 1.2.3.4:8080, got %s", addr)
} }
} }