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 }