mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-08-03 18:22:25 +02:00
If a peer has, or used to have a channel with us there's no need to check for the ban score.
561 lines
16 KiB
Go
561 lines
16 KiB
Go
package lnd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"sync"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btclog/v2"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/lnutils"
|
|
)
|
|
|
|
// accessMan is responsible for managing the server's access permissions.
|
|
type accessMan struct {
|
|
cfg *accessManConfig
|
|
|
|
// banScoreMtx is used for the server's ban tracking. If the server
|
|
// mutex is also going to be locked, ensure that this is locked after
|
|
// the server mutex.
|
|
banScoreMtx sync.RWMutex
|
|
|
|
// peerCounts is a mapping from remote public key to {bool, uint64}
|
|
// where the bool indicates that we have an open/closed channel with
|
|
// the peer and where the uint64 indicates the number of pending-open
|
|
// channels we currently have with them. This mapping will be used to
|
|
// determine access permissions for the peer. The map key is the
|
|
// string-version of the serialized public key.
|
|
//
|
|
// NOTE: This MUST be accessed with the banScoreMtx held.
|
|
peerCounts map[string]channeldb.ChanCount
|
|
|
|
// peerScores stores each connected peer's access status. The map key
|
|
// is the string-version of the serialized public key.
|
|
//
|
|
// NOTE: This MUST be accessed with the banScoreMtx held.
|
|
peerScores map[string]peerSlotStatus
|
|
|
|
// numRestricted tracks the number of peers with restricted access in
|
|
// peerScores. This MUST be accessed with the banScoreMtx held.
|
|
numRestricted int64
|
|
}
|
|
|
|
type accessManConfig struct {
|
|
// initAccessPerms checks the channeldb for initial access permissions
|
|
// and then populates the peerCounts and peerScores maps.
|
|
initAccessPerms func() (map[string]channeldb.ChanCount, error)
|
|
|
|
// shouldDisconnect determines whether we should disconnect a peer or
|
|
// not.
|
|
shouldDisconnect func(*btcec.PublicKey) (bool, error)
|
|
|
|
// maxRestrictedSlots is the number of restricted slots we'll allocate.
|
|
maxRestrictedSlots int64
|
|
}
|
|
|
|
func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
|
|
a := &accessMan{
|
|
cfg: cfg,
|
|
peerCounts: make(map[string]channeldb.ChanCount),
|
|
peerScores: make(map[string]peerSlotStatus),
|
|
}
|
|
|
|
counts, err := a.cfg.initAccessPerms()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We'll populate the server's peerCounts map with the counts fetched
|
|
// via initAccessPerms. Also note that we haven't yet connected to the
|
|
// peers.
|
|
maps.Copy(a.peerCounts, counts)
|
|
|
|
acsmLog.Info("Access Manager initialized")
|
|
|
|
return a, nil
|
|
}
|
|
|
|
// assignPeerPerms assigns a new peer its permissions. This does not track the
|
|
// access in the maps. This is intentional.
|
|
func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
|
|
peerAccessStatus, error) {
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
acsmLog.DebugS(ctx, "Assigning permissions")
|
|
|
|
// Default is restricted unless the below filters say otherwise.
|
|
access := peerStatusRestricted
|
|
|
|
// Lock banScoreMtx for reading so that we can update the banning maps
|
|
// below.
|
|
a.banScoreMtx.RLock()
|
|
if count, found := a.peerCounts[peerMapKey]; found {
|
|
if count.HasOpenOrClosedChan {
|
|
acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
|
|
"assigning protected access")
|
|
|
|
access = peerStatusProtected
|
|
} else if count.PendingOpenCount != 0 {
|
|
acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
|
|
"assigning temporary access")
|
|
|
|
access = peerStatusTemporary
|
|
}
|
|
}
|
|
a.banScoreMtx.RUnlock()
|
|
|
|
// Exit early if the peer status is no longer restricted.
|
|
if access != peerStatusRestricted {
|
|
return access, nil
|
|
}
|
|
|
|
// Check whether this peer is banned.
|
|
shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
|
|
if err != nil {
|
|
acsmLog.ErrorS(ctx, "Error checking disconnect status", err)
|
|
|
|
// Access is restricted here.
|
|
return access, err
|
|
}
|
|
|
|
if shouldDisconnect {
|
|
acsmLog.WarnS(ctx, "Peer is banned, assigning restricted access",
|
|
ErrGossiperBan)
|
|
|
|
// Access is restricted here.
|
|
return access, ErrGossiperBan
|
|
}
|
|
|
|
// If we've reached this point and access hasn't changed from
|
|
// restricted, then we need to check if we even have a slot for this
|
|
// peer.
|
|
acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access")
|
|
|
|
a.banScoreMtx.RLock()
|
|
defer a.banScoreMtx.RUnlock()
|
|
|
|
if a.numRestricted >= a.cfg.maxRestrictedSlots {
|
|
acsmLog.WarnS(ctx, "No more restricted slots available, "+
|
|
"denying peer", ErrNoMoreRestrictedAccessSlots,
|
|
"num_restricted", a.numRestricted, "max_restricted",
|
|
a.cfg.maxRestrictedSlots)
|
|
|
|
return access, ErrNoMoreRestrictedAccessSlots
|
|
}
|
|
|
|
return access, nil
|
|
}
|
|
|
|
// newPendingOpenChan is called after the pending-open channel has been
|
|
// committed to the database. This may transition a restricted-access peer to a
|
|
// temporary-access peer.
|
|
func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
|
|
a.banScoreMtx.Lock()
|
|
defer a.banScoreMtx.Unlock()
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
acsmLog.DebugS(ctx, "Processing new pending open channel")
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
// Fetch the peer's access status from peerScores.
|
|
status, found := a.peerScores[peerMapKey]
|
|
if !found {
|
|
acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
|
|
|
|
// If we didn't find the peer, we'll return an error.
|
|
return ErrNoPeerScore
|
|
}
|
|
|
|
switch status.state {
|
|
case peerStatusProtected:
|
|
acsmLog.DebugS(ctx, "Peer already protected, no change")
|
|
|
|
// If this peer's access status is protected, we don't need to
|
|
// do anything.
|
|
return nil
|
|
|
|
case peerStatusTemporary:
|
|
// If this peer's access status is temporary, we'll need to
|
|
// update the peerCounts map. The peer's access status will stay
|
|
// temporary.
|
|
peerCount, found := a.peerCounts[peerMapKey]
|
|
if !found {
|
|
// Error if we did not find any info in peerCounts.
|
|
acsmLog.ErrorS(ctx, "Pending peer info not found",
|
|
ErrNoPendingPeerInfo)
|
|
|
|
return ErrNoPendingPeerInfo
|
|
}
|
|
|
|
// Increment the pending channel amount.
|
|
peerCount.PendingOpenCount += 1
|
|
a.peerCounts[peerMapKey] = peerCount
|
|
|
|
acsmLog.DebugS(ctx, "Peer is temporary, incremented "+
|
|
"pending count",
|
|
"pending_count", peerCount.PendingOpenCount)
|
|
|
|
case peerStatusRestricted:
|
|
// If the peer's access status is restricted, then we can
|
|
// transition it to a temporary-access peer. We'll need to
|
|
// update numRestricted and also peerScores. We'll also need to
|
|
// update peerCounts.
|
|
peerCount := channeldb.ChanCount{
|
|
HasOpenOrClosedChan: false,
|
|
PendingOpenCount: 1,
|
|
}
|
|
|
|
a.peerCounts[peerMapKey] = peerCount
|
|
|
|
// A restricted-access slot has opened up.
|
|
oldRestricted := a.numRestricted
|
|
a.numRestricted -= 1
|
|
|
|
a.peerScores[peerMapKey] = peerSlotStatus{
|
|
state: peerStatusTemporary,
|
|
}
|
|
|
|
acsmLog.InfoS(ctx, "Peer transitioned restricted -> "+
|
|
"temporary (pending open)",
|
|
"old_restricted", oldRestricted,
|
|
"new_restricted", a.numRestricted)
|
|
|
|
default:
|
|
// This should not be possible.
|
|
err := fmt.Errorf("invalid peer access status %v for %x",
|
|
status.state, peerMapKey)
|
|
acsmLog.ErrorS(ctx, "Invalid peer access status", err)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// newPendingCloseChan is called when a pending-open channel prematurely closes
|
|
// before the funding transaction has confirmed. This potentially demotes a
|
|
// temporary-access peer to a restricted-access peer. If no restricted-access
|
|
// slots are available, the peer will be disconnected.
|
|
func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
|
|
a.banScoreMtx.Lock()
|
|
defer a.banScoreMtx.Unlock()
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
acsmLog.DebugS(ctx, "Processing pending channel close")
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
// Fetch the peer's access status from peerScores.
|
|
status, found := a.peerScores[peerMapKey]
|
|
if !found {
|
|
acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
|
|
|
|
return ErrNoPeerScore
|
|
}
|
|
|
|
switch status.state {
|
|
case peerStatusProtected:
|
|
// If this peer is protected, we don't do anything.
|
|
acsmLog.DebugS(ctx, "Peer is protected, no change")
|
|
|
|
return nil
|
|
|
|
case peerStatusTemporary:
|
|
// If this peer is temporary, we need to check if it will
|
|
// revert to a restricted-access peer.
|
|
peerCount, found := a.peerCounts[peerMapKey]
|
|
if !found {
|
|
acsmLog.ErrorS(ctx, "Pending peer info not found",
|
|
ErrNoPendingPeerInfo)
|
|
|
|
// Error if we did not find any info in peerCounts.
|
|
return ErrNoPendingPeerInfo
|
|
}
|
|
|
|
currentNumPending := peerCount.PendingOpenCount - 1
|
|
|
|
acsmLog.DebugS(ctx, "Peer is temporary, decrementing "+
|
|
"pending count",
|
|
"pending_count", currentNumPending)
|
|
|
|
if currentNumPending == 0 {
|
|
// Remove the entry from peerCounts.
|
|
delete(a.peerCounts, peerMapKey)
|
|
|
|
// If this is the only pending-open channel for this
|
|
// peer and it's getting removed, attempt to demote
|
|
// this peer to a restricted peer.
|
|
if a.numRestricted == a.cfg.maxRestrictedSlots {
|
|
// There are no available restricted slots, so
|
|
// we need to disconnect this peer. We leave
|
|
// this up to the caller.
|
|
acsmLog.WarnS(ctx, "Peer last pending "+
|
|
"channel closed: ",
|
|
ErrNoMoreRestrictedAccessSlots,
|
|
"num_restricted", a.numRestricted,
|
|
"max_restricted", a.cfg.maxRestrictedSlots)
|
|
|
|
return ErrNoMoreRestrictedAccessSlots
|
|
}
|
|
|
|
// Otherwise, there is an available restricted-access
|
|
// slot, so we can demote this peer.
|
|
a.peerScores[peerMapKey] = peerSlotStatus{
|
|
state: peerStatusRestricted,
|
|
}
|
|
|
|
// Update numRestricted.
|
|
oldRestricted := a.numRestricted
|
|
a.numRestricted++
|
|
|
|
acsmLog.InfoS(ctx, "Peer transitioned "+
|
|
"temporary -> restricted "+
|
|
"(last pending closed)",
|
|
"old_restricted", oldRestricted,
|
|
"new_restricted", a.numRestricted)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Else, we don't need to demote this peer since it has other
|
|
// pending-open channels with us.
|
|
peerCount.PendingOpenCount = currentNumPending
|
|
a.peerCounts[peerMapKey] = peerCount
|
|
|
|
acsmLog.DebugS(ctx, "Peer still has other pending channels",
|
|
"pending_count", currentNumPending)
|
|
|
|
return nil
|
|
|
|
case peerStatusRestricted:
|
|
// This should not be possible. This indicates an error.
|
|
err := fmt.Errorf("invalid peer access state transition: "+
|
|
"pending close for restricted peer %x", peerMapKey)
|
|
acsmLog.ErrorS(ctx, "Invalid peer access state transition", err)
|
|
|
|
return err
|
|
|
|
default:
|
|
// This should not be possible.
|
|
err := fmt.Errorf("invalid peer access status %v for %x",
|
|
status.state, peerMapKey)
|
|
acsmLog.ErrorS(ctx, "Invalid peer access status", err)
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
// newOpenChan is called when a pending-open channel becomes an open channel
|
|
// (i.e. the funding transaction has confirmed). If the remote peer is a
|
|
// temporary-access peer, it will be promoted to a protected-access peer.
|
|
func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
|
|
a.banScoreMtx.Lock()
|
|
defer a.banScoreMtx.Unlock()
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
acsmLog.DebugS(ctx, "Processing new open channel")
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
// Fetch the peer's access status from peerScores.
|
|
status, found := a.peerScores[peerMapKey]
|
|
if !found {
|
|
// If we didn't find the peer, we'll return an error.
|
|
acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
|
|
|
|
return ErrNoPeerScore
|
|
}
|
|
|
|
switch status.state {
|
|
case peerStatusProtected:
|
|
acsmLog.DebugS(ctx, "Peer already protected, no change")
|
|
|
|
// If the peer's state is already protected, we don't need to do
|
|
// anything more.
|
|
return nil
|
|
|
|
case peerStatusTemporary:
|
|
// If the peer's state is temporary, we'll upgrade the peer to
|
|
// a protected peer.
|
|
peerCount, found := a.peerCounts[peerMapKey]
|
|
if !found {
|
|
// Error if we did not find any info in peerCounts.
|
|
acsmLog.ErrorS(ctx, "Pending peer info not found",
|
|
ErrNoPendingPeerInfo)
|
|
|
|
return ErrNoPendingPeerInfo
|
|
}
|
|
|
|
peerCount.HasOpenOrClosedChan = true
|
|
a.peerCounts[peerMapKey] = peerCount
|
|
|
|
newStatus := peerSlotStatus{
|
|
state: peerStatusProtected,
|
|
}
|
|
a.peerScores[peerMapKey] = newStatus
|
|
|
|
acsmLog.InfoS(ctx, "Peer transitioned temporary -> "+
|
|
"protected (channel opened)")
|
|
|
|
return nil
|
|
|
|
case peerStatusRestricted:
|
|
// This should not be possible. For the server to receive a
|
|
// state-transition event via NewOpenChan, the server must have
|
|
// previously granted this peer "temporary" access. This
|
|
// temporary access would not have been revoked or downgraded
|
|
// without `CloseChannel` being called with the pending
|
|
// argument set to true. This means that an open-channel state
|
|
// transition would be impossible. Therefore, we can return an
|
|
// error.
|
|
err := fmt.Errorf("invalid peer access status: new open "+
|
|
"channel for restricted peer %x", peerMapKey)
|
|
|
|
acsmLog.ErrorS(ctx, "Invalid peer access status", err)
|
|
|
|
return err
|
|
|
|
default:
|
|
// This should not be possible.
|
|
err := fmt.Errorf("invalid peer access status %v for %x",
|
|
status.state, peerMapKey)
|
|
|
|
acsmLog.ErrorS(ctx, "Invalid peer access status", err)
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
// checkIncomingConnBanScore checks whether, given the remote's public hex-
|
|
// encoded key, we should not accept this incoming connection or immediately
|
|
// disconnect. This does not assign to the server's peerScores maps. This is
|
|
// just an inbound filter that the brontide listeners use.
|
|
func (a *accessMan) checkIncomingConnBanScore(remotePub *btcec.PublicKey) (
|
|
bool, error) {
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
acsmLog.TraceS(ctx, "Checking incoming connection ban score")
|
|
|
|
a.banScoreMtx.RLock()
|
|
defer a.banScoreMtx.RUnlock()
|
|
|
|
if _, found := a.peerCounts[peerMapKey]; !found {
|
|
acsmLog.DebugS(ctx, "Peer not found in counts, "+
|
|
"checking restricted slots")
|
|
|
|
// Check numRestricted to see if there is an available slot. In
|
|
// the future, it's possible to add better heuristics.
|
|
if a.numRestricted < a.cfg.maxRestrictedSlots {
|
|
// There is an available slot.
|
|
acsmLog.DebugS(ctx, "Restricted slot available, "+
|
|
"accepting",
|
|
"num_restricted", a.numRestricted,
|
|
"max_restricted", a.cfg.maxRestrictedSlots)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// If there are no slots left, then we reject this connection.
|
|
acsmLog.WarnS(ctx, "No restricted slots available, "+
|
|
"rejecting",
|
|
ErrNoMoreRestrictedAccessSlots,
|
|
"num_restricted", a.numRestricted,
|
|
"max_restricted", a.cfg.maxRestrictedSlots)
|
|
|
|
return false, ErrNoMoreRestrictedAccessSlots
|
|
}
|
|
|
|
// Else, the peer is either protected or temporary.
|
|
acsmLog.DebugS(ctx, "Peer found (protected/temporary), accepting")
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// addPeerAccess tracks a peer's access in the maps. This should be called when
|
|
// the peer has fully connected.
|
|
func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
|
|
access peerAccessStatus) {
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
acsmLog.DebugS(ctx, "Adding peer access", "access", access)
|
|
|
|
// Add the remote public key to peerScores.
|
|
a.banScoreMtx.Lock()
|
|
defer a.banScoreMtx.Unlock()
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
a.peerScores[peerMapKey] = peerSlotStatus{state: access}
|
|
|
|
// Increment numRestricted.
|
|
if access == peerStatusRestricted {
|
|
oldRestricted := a.numRestricted
|
|
a.numRestricted++
|
|
|
|
acsmLog.DebugS(ctx, "Incremented restricted slots",
|
|
"old_restricted", oldRestricted,
|
|
"new_restricted", a.numRestricted)
|
|
}
|
|
}
|
|
|
|
// removePeerAccess removes the peer's access from the maps. This should be
|
|
// called when the peer has been disconnected.
|
|
func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) {
|
|
a.banScoreMtx.Lock()
|
|
defer a.banScoreMtx.Unlock()
|
|
|
|
ctx := btclog.WithCtx(
|
|
context.TODO(), lnutils.LogPubKey("peer", remotePub),
|
|
)
|
|
|
|
acsmLog.DebugS(ctx, "Removing peer access")
|
|
|
|
peerMapKey := string(remotePub.SerializeCompressed())
|
|
|
|
status, found := a.peerScores[peerMapKey]
|
|
if !found {
|
|
acsmLog.InfoS(ctx, "Peer score not found during removal")
|
|
return
|
|
}
|
|
|
|
if status.state == peerStatusRestricted {
|
|
// If the status is restricted, then we decrement from
|
|
// numRestrictedSlots.
|
|
oldRestricted := a.numRestricted
|
|
a.numRestricted--
|
|
|
|
acsmLog.DebugS(ctx, "Decremented restricted slots",
|
|
"old_restricted", oldRestricted,
|
|
"new_restricted", a.numRestricted)
|
|
}
|
|
|
|
acsmLog.TraceS(ctx, "Deleting peer from peerScores")
|
|
|
|
delete(a.peerScores, peerMapKey)
|
|
}
|