mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-11 07:34:59 +03:00
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:
@@ -65,11 +65,18 @@ func AllowPrivateFromContext(ctx context.Context) bool {
|
|||||||
|
|
||||||
var defaultDialer = &net.Dialer{Timeout: 10 * time.Second}
|
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) {
|
func SSRFGuardedDialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
host, port, err := net.SplitHostPort(addr)
|
host, port, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
allowPrivate := AllowPrivateFromContext(ctx)
|
||||||
var ips []net.IPAddr
|
var ips []net.IPAddr
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
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
|
var lastErr, blockedErr error
|
||||||
for _, ipAddr := range ips {
|
for i, ipAddr := range ips {
|
||||||
if !allowPrivate && IsBlockedIP(ipAddr.IP) {
|
if !allowPrivate && IsBlockedIP(ipAddr.IP) {
|
||||||
blockedErr = fmt.Errorf("%w %s", ErrPrivateAddressBlocked, ipAddr.IP)
|
blockedErr = fmt.Errorf("%w %s", ErrPrivateAddressBlocked, ipAddr.IP)
|
||||||
continue
|
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 {
|
if derr == nil {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user