mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
Two packages were named for the same word and meant different things. network/ dialed peers, exchanged keys, throttled and tracked them; nets/ tracked which chains share a validator set and when they have all bootstrapped. One letter apart, and neither name said which was which. The peer layer is mesh — the word this estate already uses for it, in lux-rs/node's own mesh.rs. That frees network for the thing that is actually a network: a set of chains sharing a validator set. A network holds chains; a chain belongs to one network. github.com/luxfi/p2p stays what it is, the gossip library the VMs use. Three concerns, three names, none of them borrowed from another. Co-authored-by: Hanzo Dev <[email protected]>
161 lines
5.1 KiB
Go
161 lines
5.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package throttling
|
|
|
|
import (
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/luxfi/math/set"
|
|
"github.com/luxfi/timer/mockable"
|
|
|
|
timerpkg "github.com/luxfi/timer"
|
|
)
|
|
|
|
var (
|
|
_ InboundConnUpgradeThrottler = (*inboundConnUpgradeThrottler)(nil)
|
|
_ InboundConnUpgradeThrottler = (*noInboundConnUpgradeThrottler)(nil)
|
|
)
|
|
|
|
// InboundConnUpgradeThrottler returns whether we should upgrade an inbound connection from IP [ipStr].
|
|
// If ShouldUpgrade(ipStr) returns false, the connection to that IP should be closed.
|
|
// Note that InboundConnUpgradeThrottler rate-limits _upgrading_ of
|
|
// inbound connections, whereas throttledListener rate-limits
|
|
// _acceptance_ of inbound connections.
|
|
type InboundConnUpgradeThrottler interface {
|
|
// Dispatch starts this InboundConnUpgradeThrottler.
|
|
// Must be called before [ShouldUpgrade].
|
|
// Blocks until [Stop] is called (i.e. should be called in a goroutine.)
|
|
Dispatch()
|
|
// Stop this InboundConnUpgradeThrottler and causes [Dispatch] to return.
|
|
// Should be called when we're done with this InboundConnUpgradeThrottler.
|
|
// This InboundConnUpgradeThrottler must not be used after [Stop] is called.
|
|
Stop()
|
|
// Returns whether we should upgrade an inbound connection from [ipStr].
|
|
// Must only be called after [Dispatch] has been called.
|
|
// If [ip] is a local IP, this method always returns true.
|
|
// Must not be called after [Stop] has been called.
|
|
ShouldUpgrade(ip netip.AddrPort) bool
|
|
}
|
|
|
|
type InboundConnUpgradeThrottlerConfig struct {
|
|
// ShouldUpgrade(ipStr) returns true if it has been at least [UpgradeCooldown]
|
|
// since the last time ShouldUpgrade(ipStr) returned true or if
|
|
// ShouldUpgrade(ipStr) has never been called.
|
|
// If <= 0, inbound connections not rate-limited.
|
|
UpgradeCooldown time.Duration `json:"upgradeCooldown"`
|
|
// Maximum number of inbound connections upgraded within [UpgradeCooldown].
|
|
// (As implemented in inboundConnUpgradeThrottler, may actually upgrade
|
|
// [MaxRecentConnsUpgraded+1] due to a race condition but that's fine.)
|
|
// If <= 0, inbound connections not rate-limited.
|
|
MaxRecentConnsUpgraded int `json:"maxRecentConnsUpgraded"`
|
|
}
|
|
|
|
// Returns an InboundConnUpgradeThrottler that upgrades an inbound
|
|
// connection from a given IP at most every [UpgradeCooldown].
|
|
func NewInboundConnUpgradeThrottler(config InboundConnUpgradeThrottlerConfig) InboundConnUpgradeThrottler {
|
|
if config.UpgradeCooldown <= 0 || config.MaxRecentConnsUpgraded <= 0 {
|
|
return &noInboundConnUpgradeThrottler{}
|
|
}
|
|
return &inboundConnUpgradeThrottler{
|
|
InboundConnUpgradeThrottlerConfig: config,
|
|
done: make(chan struct{}),
|
|
recentIPs: set.NewSet[netip.Addr](config.MaxRecentConnsUpgraded),
|
|
recentIPsAndTimes: make(chan ipAndTime, config.MaxRecentConnsUpgraded),
|
|
}
|
|
}
|
|
|
|
// noInboundConnUpgradeThrottler upgrades all inbound connections
|
|
type noInboundConnUpgradeThrottler struct{}
|
|
|
|
func (*noInboundConnUpgradeThrottler) Dispatch() {}
|
|
|
|
func (*noInboundConnUpgradeThrottler) Stop() {}
|
|
|
|
func (*noInboundConnUpgradeThrottler) ShouldUpgrade(netip.AddrPort) bool {
|
|
return true
|
|
}
|
|
|
|
type ipAndTime struct {
|
|
ip netip.Addr
|
|
cooldownElapsedAt time.Time
|
|
}
|
|
|
|
type inboundConnUpgradeThrottler struct {
|
|
InboundConnUpgradeThrottlerConfig
|
|
lock sync.Mutex
|
|
// Useful for faking time in tests
|
|
clock mockable.Clock
|
|
// When [done] is closed, Dispatch returns.
|
|
done chan struct{}
|
|
// IP --> Present if ShouldUpgrade(ipStr) returned true
|
|
// within the last [UpgradeCooldown].
|
|
recentIPs set.Set[netip.Addr]
|
|
// Sorted in order of increasing time
|
|
// of last call to ShouldUpgrade that returned true.
|
|
// For each IP in this channel, ShouldUpgrade(ipStr)
|
|
// returned true within the last [UpgradeCooldown].
|
|
recentIPsAndTimes chan ipAndTime
|
|
}
|
|
|
|
// Returns whether we should upgrade an inbound connection from [ipStr].
|
|
func (n *inboundConnUpgradeThrottler) ShouldUpgrade(addrPort netip.AddrPort) bool {
|
|
// Only use addr (not port). This mitigates DoS attacks from many nodes on one
|
|
// host.
|
|
addr := addrPort.Addr()
|
|
if addr.IsLoopback() {
|
|
// Don't rate-limit loopback IPs
|
|
return true
|
|
}
|
|
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
|
|
if n.recentIPs.Contains(addr) {
|
|
// We recently upgraded an inbound connection from this IP
|
|
return false
|
|
}
|
|
|
|
select {
|
|
case n.recentIPsAndTimes <- ipAndTime{
|
|
ip: addr,
|
|
cooldownElapsedAt: n.clock.Time().Add(n.UpgradeCooldown),
|
|
}:
|
|
n.recentIPs.Add(addr)
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (n *inboundConnUpgradeThrottler) Dispatch() {
|
|
timer := timerpkg.StoppedTimer()
|
|
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case next := <-n.recentIPsAndTimes:
|
|
// Sleep until it's time to remove the next IP
|
|
timer.Reset(next.cooldownElapsedAt.Sub(n.clock.Time()))
|
|
|
|
select {
|
|
case <-timer.C:
|
|
// Remove the next IP (we'd upgrade another inbound connection from it)
|
|
n.lock.Lock()
|
|
n.recentIPs.Remove(next.ip)
|
|
n.lock.Unlock()
|
|
case <-n.done:
|
|
return
|
|
}
|
|
case <-n.done:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (n *inboundConnUpgradeThrottler) Stop() {
|
|
close(n.done)
|
|
}
|