From 9cf8c87e8da3697117e35a95415664071556cd67 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 31 Oct 2026 09:19:54 +0000 Subject: [PATCH] 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. --- internal/util/netsafe/netsafe.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/util/netsafe/netsafe.go b/internal/util/netsafe/netsafe.go index 689425d65..fd656623c 100644 --- a/internal/util/netsafe/netsafe.go +++ b/internal/util/netsafe/netsafe.go @@ -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 }