From 0522ae8155b8b012cdaf868af2f47655b55e01b6 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 21 Jun 2026 22:34:43 +0800 Subject: [PATCH] fix(net): use pure-Go DNS fallback to prevent thread exhaustion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/net/resovle.go | 23 +++++++++++++++++++++-- internal/net/resovle_test.go | 11 ++++++++--- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/internal/net/resovle.go b/internal/net/resovle.go index 96ee95ab..8e0bb460 100644 --- a/internal/net/resovle.go +++ b/internal/net/resovle.go @@ -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 } diff --git a/internal/net/resovle_test.go b/internal/net/resovle_test.go index 6726555e..adb25210 100644 --- a/internal/net/resovle_test.go +++ b/internal/net/resovle_test.go @@ -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) } }