| 
 | 1 | +package discovery  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"errors"  | 
 | 5 | +	"sync"  | 
 | 6 | +	"time"  | 
 | 7 | + | 
 | 8 | +	"github.com/btcsuite/btcd/btcec/v2"  | 
 | 9 | +	"github.com/lightninglabs/neutrino/cache"  | 
 | 10 | +	"github.com/lightninglabs/neutrino/cache/lru"  | 
 | 11 | +	"github.com/lightningnetwork/lnd/channeldb"  | 
 | 12 | +	"github.com/lightningnetwork/lnd/lnwire"  | 
 | 13 | +)  | 
 | 14 | + | 
 | 15 | +const (  | 
 | 16 | +	// maxBannedPeers limits the maximum number of banned pubkeys that  | 
 | 17 | +	// we'll store.  | 
 | 18 | +	// TODO(eugene): tune.  | 
 | 19 | +	maxBannedPeers = 10_000  | 
 | 20 | + | 
 | 21 | +	// banThreshold is the point at which non-channel peers will be banned.  | 
 | 22 | +	// TODO(eugene): tune.  | 
 | 23 | +	banThreshold = 100  | 
 | 24 | + | 
 | 25 | +	// banTime is the amount of time that the non-channel peer will be  | 
 | 26 | +	// banned for. Channel announcements from channel peers will be dropped  | 
 | 27 | +	// if it's not one of our channels.  | 
 | 28 | +	// TODO(eugene): tune.  | 
 | 29 | +	banTime = time.Hour * 48  | 
 | 30 | + | 
 | 31 | +	// resetDelta is the time after a peer's last ban update that we'll  | 
 | 32 | +	// reset its ban score.  | 
 | 33 | +	// TODO(eugene): tune.  | 
 | 34 | +	resetDelta = time.Hour * 48  | 
 | 35 | + | 
 | 36 | +	// purgeInterval is how often we'll remove entries from the  | 
 | 37 | +	// peerBanIndex and allow peers to be un-banned. This interval is also  | 
 | 38 | +	// used to reset ban scores of peers that aren't banned.  | 
 | 39 | +	purgeInterval = time.Minute * 10  | 
 | 40 | +)  | 
 | 41 | + | 
 | 42 | +var ErrPeerBanned = errors.New("peer has bypassed ban threshold - banning")  | 
 | 43 | + | 
 | 44 | +// ClosedChannelTracker handles closed channels being gossiped to us.  | 
 | 45 | +type ClosedChannelTracker interface {  | 
 | 46 | +	// GraphCloser is used to mark channels as closed and to check whether  | 
 | 47 | +	// certain channels are closed.  | 
 | 48 | +	GraphCloser  | 
 | 49 | + | 
 | 50 | +	// IsChannelPeer checks whether we have a channel with a peer.  | 
 | 51 | +	IsChannelPeer(*btcec.PublicKey) (bool, error)  | 
 | 52 | +}  | 
 | 53 | + | 
 | 54 | +// GraphCloser handles tracking closed channels by their scid.  | 
 | 55 | +type GraphCloser interface {  | 
 | 56 | +	// PutClosedScid marks a channel as closed so that we won't validate  | 
 | 57 | +	// channel announcements for it again.  | 
 | 58 | +	PutClosedScid(lnwire.ShortChannelID) error  | 
 | 59 | + | 
 | 60 | +	// IsClosedScid checks if a short channel id is closed.  | 
 | 61 | +	IsClosedScid(lnwire.ShortChannelID) (bool, error)  | 
 | 62 | +}  | 
 | 63 | + | 
 | 64 | +// NodeInfoInquirier handles queries relating to specific nodes and channels  | 
 | 65 | +// they may have with us.  | 
 | 66 | +type NodeInfoInquirer interface {  | 
 | 67 | +	// FetchOpenChannels returns the set of channels that we have with the  | 
 | 68 | +	// peer identified by the passed-in public key.  | 
 | 69 | +	FetchOpenChannels(*btcec.PublicKey) ([]*channeldb.OpenChannel, error)  | 
 | 70 | +}  | 
 | 71 | + | 
 | 72 | +// ScidCloserMan helps the gossiper handle closed channels that are in the  | 
 | 73 | +// ChannelGraph.  | 
 | 74 | +type ScidCloserMan struct {  | 
 | 75 | +	graph     GraphCloser  | 
 | 76 | +	channelDB NodeInfoInquirer  | 
 | 77 | +}  | 
 | 78 | + | 
 | 79 | +// NewScidCloserMan creates a new ScidCloserMan.  | 
 | 80 | +func NewScidCloserMan(graph GraphCloser,  | 
 | 81 | +	channelDB NodeInfoInquirer) *ScidCloserMan {  | 
 | 82 | + | 
 | 83 | +	return &ScidCloserMan{  | 
 | 84 | +		graph:     graph,  | 
 | 85 | +		channelDB: channelDB,  | 
 | 86 | +	}  | 
 | 87 | +}  | 
 | 88 | + | 
 | 89 | +// PutClosedScid marks scid as closed so the gossiper can ignore this channel  | 
 | 90 | +// in the future.  | 
 | 91 | +func (s *ScidCloserMan) PutClosedScid(scid lnwire.ShortChannelID) error {  | 
 | 92 | +	return s.graph.PutClosedScid(scid)  | 
 | 93 | +}  | 
 | 94 | + | 
 | 95 | +// IsClosedScid checks whether scid is closed so that the gossiper can ignore  | 
 | 96 | +// it.  | 
 | 97 | +func (s *ScidCloserMan) IsClosedScid(scid lnwire.ShortChannelID) (bool,  | 
 | 98 | +	error) {  | 
 | 99 | + | 
 | 100 | +	return s.graph.IsClosedScid(scid)  | 
 | 101 | +}  | 
 | 102 | + | 
 | 103 | +// IsChannelPeer checks whether we have a channel with the peer.  | 
 | 104 | +func (s *ScidCloserMan) IsChannelPeer(peerKey *btcec.PublicKey) (bool, error) {  | 
 | 105 | +	chans, err := s.channelDB.FetchOpenChannels(peerKey)  | 
 | 106 | +	if err != nil {  | 
 | 107 | +		return false, err  | 
 | 108 | +	}  | 
 | 109 | + | 
 | 110 | +	return len(chans) > 0, nil  | 
 | 111 | +}  | 
 | 112 | + | 
 | 113 | +// A compile-time constraint to ensure ScidCloserMan implements  | 
 | 114 | +// ClosedChannelTracker.  | 
 | 115 | +var _ ClosedChannelTracker = (*ScidCloserMan)(nil)  | 
 | 116 | + | 
 | 117 | +// cachedBanInfo is used to track a peer's ban score and if it is banned.  | 
 | 118 | +type cachedBanInfo struct {  | 
 | 119 | +	score      uint64  | 
 | 120 | +	lastUpdate time.Time  | 
 | 121 | +}  | 
 | 122 | + | 
 | 123 | +// Size returns the "size" of an entry.  | 
 | 124 | +func (c *cachedBanInfo) Size() (uint64, error) {  | 
 | 125 | +	return 1, nil  | 
 | 126 | +}  | 
 | 127 | + | 
 | 128 | +// isBanned returns true if the ban score is greater than the ban threshold.  | 
 | 129 | +func (c *cachedBanInfo) isBanned() bool {  | 
 | 130 | +	return c.score >= banThreshold  | 
 | 131 | +}  | 
 | 132 | + | 
 | 133 | +// banman is responsible for banning peers that are misbehaving. The banman is  | 
 | 134 | +// in-memory and will be reset upon restart of LND. If a node's pubkey is in  | 
 | 135 | +// the peerBanIndex, it has a ban score. Ban scores start at 1 and are  | 
 | 136 | +// incremented by 1 for each instance of misbehavior. It uses an LRU cache to  | 
 | 137 | +// cut down on memory usage in case there are many banned peers and to protect  | 
 | 138 | +// against DoS.  | 
 | 139 | +type banman struct {  | 
 | 140 | +	// peerBanIndex tracks our peers' ban scores and if they are banned and  | 
 | 141 | +	// for how long. The ban score is incremented when our peer gives us  | 
 | 142 | +	// gossip messages that are invalid.  | 
 | 143 | +	peerBanIndex *lru.Cache[[33]byte, *cachedBanInfo]  | 
 | 144 | + | 
 | 145 | +	wg   sync.WaitGroup  | 
 | 146 | +	quit chan struct{}  | 
 | 147 | +}  | 
 | 148 | + | 
 | 149 | +// newBanman creates a new banman with the default maxBannedPeers.  | 
 | 150 | +func newBanman() *banman {  | 
 | 151 | +	return &banman{  | 
 | 152 | +		peerBanIndex: lru.NewCache[[33]byte, *cachedBanInfo](  | 
 | 153 | +			maxBannedPeers,  | 
 | 154 | +		),  | 
 | 155 | +		quit: make(chan struct{}),  | 
 | 156 | +	}  | 
 | 157 | +}  | 
 | 158 | + | 
 | 159 | +// start kicks off the banman by calling purgeExpiredBans.  | 
 | 160 | +func (b *banman) start() {  | 
 | 161 | +	b.wg.Add(1)  | 
 | 162 | +	go b.purgeExpiredBans()  | 
 | 163 | +}  | 
 | 164 | + | 
 | 165 | +// stop halts the banman.  | 
 | 166 | +func (b *banman) stop() {  | 
 | 167 | +	close(b.quit)  | 
 | 168 | +	b.wg.Wait()  | 
 | 169 | +}  | 
 | 170 | + | 
 | 171 | +// purgeOldEntries removes ban entries if their ban has expired.  | 
 | 172 | +func (b *banman) purgeExpiredBans() {  | 
 | 173 | +	defer b.wg.Done()  | 
 | 174 | + | 
 | 175 | +	purgeTicker := time.NewTicker(purgeInterval)  | 
 | 176 | +	defer purgeTicker.Stop()  | 
 | 177 | + | 
 | 178 | +	for {  | 
 | 179 | +		select {  | 
 | 180 | +		case <-purgeTicker.C:  | 
 | 181 | +			b.purgeBanEntries()  | 
 | 182 | + | 
 | 183 | +		case <-b.quit:  | 
 | 184 | +			return  | 
 | 185 | +		}  | 
 | 186 | +	}  | 
 | 187 | +}  | 
 | 188 | + | 
 | 189 | +// purgeBanEntries does two things:  | 
 | 190 | +// - removes peers from our ban list whose ban timer is up  | 
 | 191 | +// - removes peers whose ban scores have expired.  | 
 | 192 | +func (b *banman) purgeBanEntries() {  | 
 | 193 | +	keysToRemove := make([][33]byte, 0)  | 
 | 194 | + | 
 | 195 | +	sweepEntries := func(pubkey [33]byte, banInfo *cachedBanInfo) bool {  | 
 | 196 | +		if banInfo.isBanned() {  | 
 | 197 | +			// If the peer is banned, check if the ban timer has  | 
 | 198 | +			// expired.  | 
 | 199 | +			if banInfo.lastUpdate.Add(banTime).Before(time.Now()) {  | 
 | 200 | +				keysToRemove = append(keysToRemove, pubkey)  | 
 | 201 | +			}  | 
 | 202 | + | 
 | 203 | +			return true  | 
 | 204 | +		}  | 
 | 205 | + | 
 | 206 | +		if banInfo.lastUpdate.Add(resetDelta).Before(time.Now()) {  | 
 | 207 | +			// Remove non-banned peers whose ban scores have  | 
 | 208 | +			// expired.  | 
 | 209 | +			keysToRemove = append(keysToRemove, pubkey)  | 
 | 210 | +		}  | 
 | 211 | + | 
 | 212 | +		return true  | 
 | 213 | +	}  | 
 | 214 | + | 
 | 215 | +	b.peerBanIndex.Range(sweepEntries)  | 
 | 216 | + | 
 | 217 | +	for _, key := range keysToRemove {  | 
 | 218 | +		b.peerBanIndex.Delete(key)  | 
 | 219 | +	}  | 
 | 220 | +}  | 
 | 221 | + | 
 | 222 | +// isBanned checks whether the peer identified by the pubkey is banned.  | 
 | 223 | +func (b *banman) isBanned(pubkey [33]byte) bool {  | 
 | 224 | +	banInfo, err := b.peerBanIndex.Get(pubkey)  | 
 | 225 | +	switch {  | 
 | 226 | +	case errors.Is(err, cache.ErrElementNotFound):  | 
 | 227 | +		return false  | 
 | 228 | + | 
 | 229 | +	default:  | 
 | 230 | +		return banInfo.isBanned()  | 
 | 231 | +	}  | 
 | 232 | +}  | 
 | 233 | + | 
 | 234 | +// incrementBanScore increments a peer's ban score.  | 
 | 235 | +func (b *banman) incrementBanScore(pubkey [33]byte) {  | 
 | 236 | +	banInfo, err := b.peerBanIndex.Get(pubkey)  | 
 | 237 | +	switch {  | 
 | 238 | +	case errors.Is(err, cache.ErrElementNotFound):  | 
 | 239 | +		cachedInfo := &cachedBanInfo{  | 
 | 240 | +			score:      1,  | 
 | 241 | +			lastUpdate: time.Now(),  | 
 | 242 | +		}  | 
 | 243 | +		_, _ = b.peerBanIndex.Put(pubkey, cachedInfo)  | 
 | 244 | +	default:  | 
 | 245 | +		cachedInfo := &cachedBanInfo{  | 
 | 246 | +			score:      banInfo.score + 1,  | 
 | 247 | +			lastUpdate: time.Now(),  | 
 | 248 | +		}  | 
 | 249 | + | 
 | 250 | +		_, _ = b.peerBanIndex.Put(pubkey, cachedInfo)  | 
 | 251 | +	}  | 
 | 252 | +}  | 
0 commit comments