mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-13 12:32:48 +02:00
Onion message forwarding is an unpaid side channel. Without any peer qualification the byte-bucket limiters added in the previous commits are our only defense against a Sybil attacker: an attacker that can cheaply spin up N identities and burn a full per-peer byte budget on each one saturates the global bucket and converts the aggregate cap into a service-denial primitive against legitimate channel peers. This was raised on PR review — the per-peer cap is good, but the global cap on its own is a Sybil multiplier if peer identity is free. The proper fix is to make new identities cost real capital, which is what requiring a funded channel does. This commit adds a channel-presence gate as the first check in allowOnionMessage, ahead of both the per-peer and the global rate limiters. Messages from peers that do not have at least one fully open channel with us are dropped with a new dropReasonNoChannel sentinel and never allocate any rate limiter state — the gate runs before either limiter is consulted, so no-channel peers cannot burn tokens on any bucket. Pending channels are deliberately excluded from the check: they are represented as nil values in the activeChannels map, are cheap to open and prone to getting stuck, and so do not provide the capital-cost guarantee the Sybil defense depends on. Existing Brontide cleanup paths (StopOnionActorIfExists, OnionPeerLimiter.Forget) already handle teardown on peer disconnect; nothing new is needed there because the gate keeps no-channel peers from ever allocating per-peer state in the first place. For the hot path we cannot afford to iterate the activeChannels registry on every incoming onion message, so Brontide now carries a numActiveChans atomic.Int32 that shadows the count of non-pending entries in activeChannels. hasActiveChannels is a single atomic Load and is therefore O(1). The counter is maintained in lockstep with activeChannels at every mutation site: loadActiveChannels increments it as it populates the registry during Start(); addActiveChannel uses a new lnutils.SyncMap.Swap method (a thin typed wrapper around sync.Map.Swap) to atomically replace any prior entry so that both brand-new channels and pending-to-active promotions bump the counter by exactly one; WipeChannel and handleRemovePendingChannel both use LoadAndDelete so they can inspect the prior value and only decrement when the removed entry was non-nil. Under race, this keeps the counter and the map consistent even when RPC WipeChannel races with the channelManager goroutine. The accompanying unit tests cover: the no-channel drop path at the allowOnionMessage level, asserting that neither the global stub counter nor the per-peer limiter's dropped counter move when the gate fires; the subsequent channel-gained path on the same peer, asserting the same message is accepted once hasChannel flips; and a focused Brontide-level test that walks the counter through initial emptiness, a pending-only state (counter must stay at zero), a pending-to-active promotion via direct Store + Add, the pending delete path through handleRemovePendingChannel (must not underflow), and the active delete path through LoadAndDelete + Add(-1) that WipeChannel uses internally. Running with -race confirms the Swap/LoadAndDelete patterns keep the counter and the map in sync under concurrent access.
119 lines
3.3 KiB
Go
119 lines
3.3 KiB
Go
package lnutils
|
|
|
|
import "sync"
|
|
|
|
// SyncMap wraps a sync.Map with type parameters such that it's easier to
|
|
// access the items stored in the map since no type assertion is needed. It
|
|
// also requires explicit type definition when declaring and initiating the
|
|
// variables, which helps us understanding what's stored in a given map.
|
|
type SyncMap[K comparable, V any] struct {
|
|
sync.Map
|
|
}
|
|
|
|
// Store puts an item in the map.
|
|
func (m *SyncMap[K, V]) Store(key K, value V) {
|
|
m.Map.Store(key, value)
|
|
}
|
|
|
|
// Load queries an item from the map using the specified key. If the item
|
|
// cannot be found, an empty value and false will be returned. If the stored
|
|
// item fails the type assertion, a nil value and false will be returned.
|
|
func (m *SyncMap[K, V]) Load(key K) (V, bool) {
|
|
result, ok := m.Map.Load(key)
|
|
if !ok {
|
|
return *new(V), false // nolint: gocritic
|
|
}
|
|
|
|
item, ok := result.(V)
|
|
return item, ok
|
|
}
|
|
|
|
// Delete removes an item from the map specified by the key.
|
|
func (m *SyncMap[K, V]) Delete(key K) {
|
|
m.Map.Delete(key)
|
|
}
|
|
|
|
// LoadAndDelete queries an item and deletes it from the map using the
|
|
// specified key.
|
|
func (m *SyncMap[K, V]) LoadAndDelete(key K) (V, bool) {
|
|
result, loaded := m.Map.LoadAndDelete(key)
|
|
if !loaded {
|
|
return *new(V), loaded // nolint: gocritic
|
|
}
|
|
|
|
item, ok := result.(V)
|
|
return item, ok
|
|
}
|
|
|
|
// Range iterates the map and applies the `visitor` function. If the `visitor`
|
|
// returns false, the iteration will be stopped.
|
|
func (m *SyncMap[K, V]) Range(visitor func(K, V) bool) {
|
|
m.Map.Range(func(k any, v any) bool {
|
|
return visitor(k.(K), v.(V))
|
|
})
|
|
}
|
|
|
|
// ForEach iterates the map and applies the `visitor` function. Unlike the
|
|
// `Range` method, the `visitor` function will be applied to all the items
|
|
// unless there's an error.
|
|
func (m *SyncMap[K, V]) ForEach(visitor func(K, V) error) {
|
|
// rangeVisitor wraps the `visitor` function and returns false if
|
|
// there's an error returned from the `visitor` function.
|
|
rangeVisitor := func(k K, v V) bool {
|
|
if err := visitor(k, v); err != nil {
|
|
// Break the iteration if there's an error.
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
m.Range(rangeVisitor)
|
|
}
|
|
|
|
// Len returns the number of items in the map.
|
|
func (m *SyncMap[K, V]) Len() int {
|
|
var count int
|
|
m.Range(func(_ K, _ V) bool {
|
|
count++
|
|
|
|
return true
|
|
})
|
|
|
|
return count
|
|
}
|
|
|
|
// LoadOrStore queries an item from the map using the specified key. If the
|
|
// item cannot be found, the `value` will be stored in the map and returned.
|
|
// If the stored item fails the type assertion, a nil value and false will be
|
|
// returned.
|
|
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (V, bool) {
|
|
result, loaded := m.Map.LoadOrStore(key, value)
|
|
item, ok := result.(V)
|
|
if !ok {
|
|
return *new(V), false
|
|
}
|
|
|
|
return item, loaded
|
|
}
|
|
|
|
// Swap stores value for the given key and returns the previously stored
|
|
// value (if any). The second return value reports whether a previous
|
|
// value was present. It is a thin typed wrapper around sync.Map.Swap so
|
|
// callers that need to atomically read-modify-write a map entry — for
|
|
// example, to update an atomic counter that shadows the map's
|
|
// membership — can do so without dropping down to untyped interface{}
|
|
// assertions.
|
|
func (m *SyncMap[K, V]) Swap(key K, value V) (V, bool) {
|
|
prev, loaded := m.Map.Swap(key, value)
|
|
if !loaded {
|
|
return *new(V), false
|
|
}
|
|
|
|
item, ok := prev.(V)
|
|
if !ok {
|
|
return *new(V), false
|
|
}
|
|
|
|
return item, true
|
|
}
|