github.com/ava-labs/avalanchego@v1.11.11/network/throttling/inbound_conn_upgrade_throttler.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package throttling
     5  
     6  import (
     7  	"net/netip"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/ava-labs/avalanchego/utils/logging"
    12  	"github.com/ava-labs/avalanchego/utils/set"
    13  	"github.com/ava-labs/avalanchego/utils/timer/mockable"
    14  
    15  	timerpkg "github.com/ava-labs/avalanchego/utils/timer"
    16  )
    17  
    18  var (
    19  	_ InboundConnUpgradeThrottler = (*inboundConnUpgradeThrottler)(nil)
    20  	_ InboundConnUpgradeThrottler = (*noInboundConnUpgradeThrottler)(nil)
    21  )
    22  
    23  // InboundConnUpgradeThrottler returns whether we should upgrade an inbound connection from IP [ipStr].
    24  // If ShouldUpgrade(ipStr) returns false, the connection to that IP should be closed.
    25  // Note that InboundConnUpgradeThrottler rate-limits _upgrading_ of
    26  // inbound connections, whereas throttledListener rate-limits
    27  // _acceptance_ of inbound connections.
    28  type InboundConnUpgradeThrottler interface {
    29  	// Dispatch starts this InboundConnUpgradeThrottler.
    30  	// Must be called before [ShouldUpgrade].
    31  	// Blocks until [Stop] is called (i.e. should be called in a goroutine.)
    32  	Dispatch()
    33  	// Stop this InboundConnUpgradeThrottler and causes [Dispatch] to return.
    34  	// Should be called when we're done with this InboundConnUpgradeThrottler.
    35  	// This InboundConnUpgradeThrottler must not be used after [Stop] is called.
    36  	Stop()
    37  	// Returns whether we should upgrade an inbound connection from [ipStr].
    38  	// Must only be called after [Dispatch] has been called.
    39  	// If [ip] is a local IP, this method always returns true.
    40  	// Must not be called after [Stop] has been called.
    41  	ShouldUpgrade(ip netip.AddrPort) bool
    42  }
    43  
    44  type InboundConnUpgradeThrottlerConfig struct {
    45  	// ShouldUpgrade(ipStr) returns true if it has been at least [UpgradeCooldown]
    46  	// since the last time ShouldUpgrade(ipStr) returned true or if
    47  	// ShouldUpgrade(ipStr) has never been called.
    48  	// If <= 0, inbound connections not rate-limited.
    49  	UpgradeCooldown time.Duration `json:"upgradeCooldown"`
    50  	// Maximum number of inbound connections upgraded within [UpgradeCooldown].
    51  	// (As implemented in inboundConnUpgradeThrottler, may actually upgrade
    52  	// [MaxRecentConnsUpgraded+1] due to a race condition but that's fine.)
    53  	// If <= 0, inbound connections not rate-limited.
    54  	MaxRecentConnsUpgraded int `json:"maxRecentConnsUpgraded"`
    55  }
    56  
    57  // Returns an InboundConnUpgradeThrottler that upgrades an inbound
    58  // connection from a given IP at most every [UpgradeCooldown].
    59  func NewInboundConnUpgradeThrottler(log logging.Logger, config InboundConnUpgradeThrottlerConfig) InboundConnUpgradeThrottler {
    60  	if config.UpgradeCooldown <= 0 || config.MaxRecentConnsUpgraded <= 0 {
    61  		return &noInboundConnUpgradeThrottler{}
    62  	}
    63  	return &inboundConnUpgradeThrottler{
    64  		InboundConnUpgradeThrottlerConfig: config,
    65  		log:                               log,
    66  		done:                              make(chan struct{}),
    67  		recentIPsAndTimes:                 make(chan ipAndTime, config.MaxRecentConnsUpgraded),
    68  	}
    69  }
    70  
    71  // noInboundConnUpgradeThrottler upgrades all inbound connections
    72  type noInboundConnUpgradeThrottler struct{}
    73  
    74  func (*noInboundConnUpgradeThrottler) Dispatch() {}
    75  
    76  func (*noInboundConnUpgradeThrottler) Stop() {}
    77  
    78  func (*noInboundConnUpgradeThrottler) ShouldUpgrade(netip.AddrPort) bool {
    79  	return true
    80  }
    81  
    82  type ipAndTime struct {
    83  	ip                netip.Addr
    84  	cooldownElapsedAt time.Time
    85  }
    86  
    87  type inboundConnUpgradeThrottler struct {
    88  	InboundConnUpgradeThrottlerConfig
    89  	log  logging.Logger
    90  	lock sync.Mutex
    91  	// Useful for faking time in tests
    92  	clock mockable.Clock
    93  	// When [done] is closed, Dispatch returns.
    94  	done chan struct{}
    95  	// IP --> Present if ShouldUpgrade(ipStr) returned true
    96  	// within the last [UpgradeCooldown].
    97  	recentIPs set.Set[netip.Addr]
    98  	// Sorted in order of increasing time
    99  	// of last call to ShouldUpgrade that returned true.
   100  	// For each IP in this channel, ShouldUpgrade(ipStr)
   101  	// returned true within the last [UpgradeCooldown].
   102  	recentIPsAndTimes chan ipAndTime
   103  }
   104  
   105  // Returns whether we should upgrade an inbound connection from [ipStr].
   106  func (n *inboundConnUpgradeThrottler) ShouldUpgrade(addrPort netip.AddrPort) bool {
   107  	// Only use addr (not port). This mitigates DoS attacks from many nodes on one
   108  	// host.
   109  	addr := addrPort.Addr()
   110  	if addr.IsLoopback() {
   111  		// Don't rate-limit loopback IPs
   112  		return true
   113  	}
   114  
   115  	n.lock.Lock()
   116  	defer n.lock.Unlock()
   117  
   118  	if n.recentIPs.Contains(addr) {
   119  		// We recently upgraded an inbound connection from this IP
   120  		return false
   121  	}
   122  
   123  	select {
   124  	case n.recentIPsAndTimes <- ipAndTime{
   125  		ip:                addr,
   126  		cooldownElapsedAt: n.clock.Time().Add(n.UpgradeCooldown),
   127  	}:
   128  		n.recentIPs.Add(addr)
   129  		return true
   130  	default:
   131  		return false
   132  	}
   133  }
   134  
   135  func (n *inboundConnUpgradeThrottler) Dispatch() {
   136  	timer := timerpkg.StoppedTimer()
   137  
   138  	defer timer.Stop()
   139  	for {
   140  		select {
   141  		case next := <-n.recentIPsAndTimes:
   142  			// Sleep until it's time to remove the next IP
   143  			timer.Reset(next.cooldownElapsedAt.Sub(n.clock.Time()))
   144  
   145  			select {
   146  			case <-timer.C:
   147  				// Remove the next IP (we'd upgrade another inbound connection from it)
   148  				n.lock.Lock()
   149  				n.recentIPs.Remove(next.ip)
   150  				n.lock.Unlock()
   151  			case <-n.done:
   152  				return
   153  			}
   154  		case <-n.done:
   155  			return
   156  		}
   157  	}
   158  }
   159  
   160  func (n *inboundConnUpgradeThrottler) Stop() {
   161  	close(n.done)
   162  }