Skip to content

Commit 1bf7ad9

Browse files
authored
Merge pull request #9009 from Crypt-iQ/gossip_ban_8132024
discovery: implement banning for invalid channel anns
2 parents 64b8c62 + 95acc78 commit 1bf7ad9

23 files changed

+1012
-146
lines changed

channeldb/error.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ var (
4343
// created.
4444
ErrMetaNotFound = fmt.Errorf("unable to locate meta information")
4545

46+
// ErrClosedScidsNotFound is returned when the closed scid bucket
47+
// hasn't been created.
48+
ErrClosedScidsNotFound = fmt.Errorf("closed scid bucket doesn't exist")
49+
4650
// ErrGraphNotFound is returned when at least one of the components of
4751
// graph doesn't exist.
4852
ErrGraphNotFound = fmt.Errorf("graph bucket not initialized")

channeldb/graph.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,14 @@ var (
153153
// case we'll remove all entries from the prune log with a block height
154154
// that no longer exists.
155155
pruneLogBucket = []byte("prune-log")
156+
157+
// closedScidBucket is a top-level bucket that stores scids for
158+
// channels that we know to be closed. This is used so that we don't
159+
// need to perform expensive validation checks if we receive a channel
160+
// announcement for the channel again.
161+
//
162+
// maps: scid -> []byte{}
163+
closedScidBucket = []byte("closed-scid")
156164
)
157165

158166
const (
@@ -318,6 +326,7 @@ var graphTopLevelBuckets = [][]byte{
318326
nodeBucket,
319327
edgeBucket,
320328
graphMetaBucket,
329+
closedScidBucket,
321330
}
322331

323332
// Wipe completely deletes all saved state within all used buckets within the
@@ -3884,6 +3893,53 @@ func (c *ChannelGraph) NumZombies() (uint64, error) {
38843893
return numZombies, nil
38853894
}
38863895

3896+
// PutClosedScid stores a SCID for a closed channel in the database. This is so
3897+
// that we can ignore channel announcements that we know to be closed without
3898+
// having to validate them and fetch a block.
3899+
func (c *ChannelGraph) PutClosedScid(scid lnwire.ShortChannelID) error {
3900+
return kvdb.Update(c.db, func(tx kvdb.RwTx) error {
3901+
closedScids, err := tx.CreateTopLevelBucket(closedScidBucket)
3902+
if err != nil {
3903+
return err
3904+
}
3905+
3906+
var k [8]byte
3907+
byteOrder.PutUint64(k[:], scid.ToUint64())
3908+
3909+
return closedScids.Put(k[:], []byte{})
3910+
}, func() {})
3911+
}
3912+
3913+
// IsClosedScid checks whether a channel identified by the passed in scid is
3914+
// closed. This helps avoid having to perform expensive validation checks.
3915+
// TODO: Add an LRU cache to cut down on disc reads.
3916+
func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) {
3917+
var isClosed bool
3918+
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
3919+
closedScids := tx.ReadBucket(closedScidBucket)
3920+
if closedScids == nil {
3921+
return ErrClosedScidsNotFound
3922+
}
3923+
3924+
var k [8]byte
3925+
byteOrder.PutUint64(k[:], scid.ToUint64())
3926+
3927+
if closedScids.Get(k[:]) != nil {
3928+
isClosed = true
3929+
return nil
3930+
}
3931+
3932+
return nil
3933+
}, func() {
3934+
isClosed = false
3935+
})
3936+
if err != nil {
3937+
return false, err
3938+
}
3939+
3940+
return isClosed, nil
3941+
}
3942+
38873943
func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // nolint:dupl
38883944
updateIndex kvdb.RwBucket, node *LightningNode) error {
38893945

channeldb/graph_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4037,3 +4037,28 @@ func TestGraphLoading(t *testing.T) {
40374037
graphReloaded.graphCache.nodeFeatures,
40384038
)
40394039
}
4040+
4041+
// TestClosedScid tests that we can correctly insert a SCID into the index of
4042+
// closed short channel ids.
4043+
func TestClosedScid(t *testing.T) {
4044+
t.Parallel()
4045+
4046+
graph, err := MakeTestGraph(t)
4047+
require.Nil(t, err)
4048+
4049+
scid := lnwire.ShortChannelID{}
4050+
4051+
// The scid should not exist in the closedScidBucket.
4052+
exists, err := graph.IsClosedScid(scid)
4053+
require.Nil(t, err)
4054+
require.False(t, exists)
4055+
4056+
// After we call PutClosedScid, the call to IsClosedScid should return
4057+
// true.
4058+
err = graph.PutClosedScid(scid)
4059+
require.Nil(t, err)
4060+
4061+
exists, err = graph.IsClosedScid(scid)
4062+
require.Nil(t, err)
4063+
require.True(t, exists)
4064+
}

discovery/ban.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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

Comments
 (0)