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:
+21
-2
@@ -13,7 +13,8 @@ import (
|
||||
|
||||
// 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
|
||||
// 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) {
|
||||
if addr == "" {
|
||||
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 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
|
||||
}
|
||||
|
||||
@@ -115,13 +115,18 @@ func TestResolve_ResolverEmptyResult(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()
|
||||
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 {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if addr != "example.com:8080" {
|
||||
t.Errorf("expected example.com:8080, got %s", addr)
|
||||
if addr != "1.2.3.4:8080" {
|
||||
t.Errorf("expected 1.2.3.4:8080, got %s", addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user