fix(netsafe): give a guarded dial one time budget for every address

SSRFGuardedDialContext tried each address of a name in turn with a fresh
10 s, so a name resolving to thousands of unanswering addresses held a
dial (and the caller's goroutine) for hours; a node can choose such a
name for itself. The whole call now shares one 10 s budget, divided over
the remaining addresses with net.Dialer's 2 s floor. Multi-address DNS
answers cannot be produced in a unit test here, so no test reaches it.
This commit is contained in:
MHSanaei
2026-10-31 09:19:54 +00:00
parent 5a1ea42785
commit 9cf8c87e8d
+17 -2
View File
@@ -65,11 +65,18 @@ func AllowPrivateFromContext(ctx context.Context) bool {
var defaultDialer = &net.Dialer{Timeout: 10 * time.Second}
// minDialAttempt is net.Dialer's floor for one address of a name.
const minDialAttempt = 2 * time.Second
func SSRFGuardedDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
// One budget for the whole call, shared by the addresses as net.Dialer does: a name the
// peer controls may resolve to thousands of addresses that never answer.
ctx, cancel := context.WithTimeout(ctx, defaultDialer.Timeout)
defer cancel()
allowPrivate := AllowPrivateFromContext(ctx)
var ips []net.IPAddr
if ip := net.ParseIP(host); ip != nil {
@@ -81,12 +88,20 @@ func SSRFGuardedDialContext(ctx context.Context, network, addr string) (net.Conn
}
}
var lastErr, blockedErr error
for _, ipAddr := range ips {
for i, ipAddr := range ips {
if !allowPrivate && IsBlockedIP(ipAddr.IP) {
blockedErr = fmt.Errorf("%w %s", ErrPrivateAddressBlocked, ipAddr.IP)
continue
}
conn, derr := defaultDialer.DialContext(ctx, network, net.JoinHostPort(ipAddr.IP.String(), port))
deadline, _ := ctx.Deadline()
remaining := time.Until(deadline)
if remaining <= 0 {
break
}
attempt := max(remaining/time.Duration(len(ips)-i), min(minDialAttempt, remaining))
attemptCtx, attemptCancel := context.WithTimeout(ctx, attempt)
conn, derr := defaultDialer.DialContext(attemptCtx, network, net.JoinHostPort(ipAddr.IP.String(), port))
attemptCancel()
if derr == nil {
return conn, nil
}