fix: dial IPv6 hosts by enabling dual-stack and trying every resolved address

This commit is contained in:
aykhans committed 2026-09-23 01:03:42 +04:00
1 parent d4c7dcc42c
commit 9b0e7c4304
2 files changed
+55 -29

No files matched your search

+54 -28
View File
@@ -77,8 +77,9 @@ func NewHostClients(
} }
client := &fasthttp.HostClient{ client := &fasthttp.HostClient{
MaxConns: safeUintToInt(maxConns), MaxConns: safeUintToInt(maxConns),
IsTLS: isTLS, IsTLS: isTLS,
DialDualStack: true,
TLSConfig: &tls.Config{ TLSConfig: &tls.Config{
InsecureSkipVerify: skipVerify, //nolint:gosec InsecureSkipVerify: skipVerify, //nolint:gosec
}, },
@@ -164,43 +165,68 @@ func fasthttpSocksDialer(ctx context.Context, proxyURL *url.URL, resolveLocally
return func(addr string, timeout time.Duration) (net.Conn, error) { return func(addr string, timeout time.Duration) (net.Conn, error) {
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
addrs := []string{addr}
if resolveLocally { if resolveLocally {
host, port, err := net.SplitHostPort(addr) var err error
addrs, err = resolveIPv4First(ctx, addr, timeout)
if err != nil { if err != nil {
return nil, types.NewProxyDialError(proxyStr, err) return nil, types.NewProxyDialError(proxyStr, err)
} }
}
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout) // Try every resolved address, so one unreachable family does not fail the dial.
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host) var lastErr error
dnsCancel() for _, candidate := range addrs {
if err != nil { remaining := time.Until(deadline)
return nil, types.NewProxyDialError(proxyStr, err) if remaining <= 0 {
} return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
if len(ips) == 0 {
return nil, types.NewProxyDialError(proxyStr, types.NewProxyResolveError(host))
} }
// Use the first resolved IP dialCtx, dialCancel := context.WithTimeout(ctx, remaining)
addr = net.JoinHostPort(ips[0].String(), port) conn, err := contextDialer.DialContext(dialCtx, "tcp", candidate)
dialCancel()
if err == nil {
return conn, nil
}
lastErr = err
} }
return nil, types.NewProxyDialError(proxyStr, lastErr)
// Use remaining time for dial
remaining := time.Until(deadline)
if remaining <= 0 {
return nil, types.NewProxyDialError(proxyStr, context.DeadlineExceeded)
}
dialCtx, dialCancel := context.WithTimeout(ctx, remaining)
defer dialCancel()
conn, err := contextDialer.DialContext(dialCtx, "tcp", addr)
if err != nil {
return nil, types.NewProxyDialError(proxyStr, err)
}
return conn, nil
}, nil }, nil
} }
// resolveIPv4First resolves the host of addr into candidate addresses, IPv4 ones first.
// It can return the following errors:
// - types.ProxyResolveError
func resolveIPv4First(ctx context.Context, addr string, timeout time.Duration) ([]string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout)
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
dnsCancel()
if err != nil {
return nil, err
}
if len(ips) == 0 {
return nil, types.NewProxyResolveError(host)
}
addrs := make([]string, 0, len(ips))
for _, ip := range ips {
if ip.To4() != nil {
addrs = append(addrs, net.JoinHostPort(ip.String(), port))
}
}
for _, ip := range ips {
if ip.To4() == nil {
addrs = append(addrs, net.JoinHostPort(ip.String(), port))
}
}
return addrs, nil
}
// fasthttpConnectDialer creates a dial function that tunnels through an HTTP proxy with CONNECT, // fasthttpConnectDialer creates a dial function that tunnels through an HTTP proxy with CONNECT,
// reaching the proxy over TLS when useTLS is set. // reaching the proxy over TLS when useTLS is set.
// //
+1 -1
View File
@@ -29,7 +29,7 @@ var _ script.HTTPDoer = (*scriptHTTPClient)(nil)
// - types.ProxyDialError // - types.ProxyDialError
func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) { func newScriptHTTPClients(ctx context.Context, proxies []url.URL, maxConns uint) ([]*scriptHTTPClient, error) {
if len(proxies) == 0 { if len(proxies) == 0 {
return []*scriptHTTPClient{newScriptHTTPClient(fasthttp.DialTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil return []*scriptHTTPClient{newScriptHTTPClient(fasthttp.DialDualStackTimeout, scriptHTTPDefaultTimeout, maxConns)}, nil
} }
clients := make([]*scriptHTTPClient, 0, len(proxies)) clients := make([]*scriptHTTPClient, 0, len(proxies))