Skip to content

Commit 1cebfed

Browse files
authored
Merge pull request #9607 from Roasbeef/unified-gossip-limiter
discovery: unify rate.Limiter across all gossip peers
2 parents 67d2eac + 8c3c53f commit 1cebfed

File tree

10 files changed

+432
-278
lines changed

10 files changed

+432
-278
lines changed

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,8 @@ func DefaultConfig() Config {
707707
ChannelUpdateInterval: discovery.DefaultChannelUpdateInterval,
708708
SubBatchDelay: discovery.DefaultSubBatchDelay,
709709
AnnouncementConf: discovery.DefaultProofMatureDelta,
710+
MsgRateBytes: discovery.DefaultMsgBytesPerSecond,
711+
MsgBurstBytes: discovery.DefaultMsgBytesBurst,
710712
},
711713
Invoices: &lncfg.Invoices{
712714
HoldExpiryDelta: lncfg.DefaultHoldInvoiceExpiryDelta,

discovery/gossiper.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,14 @@ type Config struct {
390390
// spent-ness of channel outpoints. For neutrino, this saves long
391391
// rescans from blocking initial usage of the daemon.
392392
AssumeChannelValid bool
393+
394+
// MsgRateBytes is the rate limit for the number of bytes per second
395+
// that we'll allocate to outbound gossip messages.
396+
MsgRateBytes uint64
397+
398+
// MsgBurstBytes is the allotted burst amount in bytes. This is the
399+
// number of starting tokens in our token bucket algorithm.
400+
MsgBurstBytes uint64
393401
}
394402

395403
// processedNetworkMsg is a wrapper around networkMsg and a boolean. It is
@@ -574,16 +582,18 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper
574582
gossiper.vb = NewValidationBarrier(1000, gossiper.quit)
575583

576584
gossiper.syncMgr = newSyncManager(&SyncManagerCfg{
577-
ChainHash: cfg.ChainHash,
578-
ChanSeries: cfg.ChanSeries,
579-
RotateTicker: cfg.RotateTicker,
580-
HistoricalSyncTicker: cfg.HistoricalSyncTicker,
581-
NumActiveSyncers: cfg.NumActiveSyncers,
582-
NoTimestampQueries: cfg.NoTimestampQueries,
583-
IgnoreHistoricalFilters: cfg.IgnoreHistoricalFilters,
584-
BestHeight: gossiper.latestHeight,
585-
PinnedSyncers: cfg.PinnedSyncers,
586-
IsStillZombieChannel: cfg.IsStillZombieChannel,
585+
ChainHash: cfg.ChainHash,
586+
ChanSeries: cfg.ChanSeries,
587+
RotateTicker: cfg.RotateTicker,
588+
HistoricalSyncTicker: cfg.HistoricalSyncTicker,
589+
NumActiveSyncers: cfg.NumActiveSyncers,
590+
NoTimestampQueries: cfg.NoTimestampQueries,
591+
IgnoreHistoricalFilters: cfg.IgnoreHistoricalFilters,
592+
BestHeight: gossiper.latestHeight,
593+
PinnedSyncers: cfg.PinnedSyncers,
594+
IsStillZombieChannel: cfg.IsStillZombieChannel,
595+
AllotedMsgBytesPerSecond: cfg.MsgRateBytes,
596+
AllotedMsgBytesBurst: cfg.MsgBurstBytes,
587597
})
588598

589599
gossiper.reliableSender = newReliableSender(&reliableSenderCfg{

discovery/sync_manager.go

Lines changed: 152 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package discovery
22

33
import (
4+
"context"
45
"errors"
56
"sync"
67
"sync/atomic"
@@ -11,6 +12,7 @@ import (
1112
"github.com/lightningnetwork/lnd/lnwire"
1213
"github.com/lightningnetwork/lnd/routing/route"
1314
"github.com/lightningnetwork/lnd/ticker"
15+
"golang.org/x/time/rate"
1416
)
1517

1618
const (
@@ -25,6 +27,21 @@ const (
2527

2628
// filterSemaSize is the capacity of gossipFilterSema.
2729
filterSemaSize = 5
30+
31+
// DefaultMsgBytesBurst is the allotted burst in bytes we'll permit.
32+
// This is the most that can be sent in a given go. Requests beyond
33+
// this, will block indefinitely. Once tokens (bytes are depleted),
34+
// they'll be refilled at the DefaultMsgBytesPerSecond rate.
35+
DefaultMsgBytesBurst = 2 * 100 * 1_024
36+
37+
// DefaultMsgBytesPerSecond is the max bytes/s we'll permit for outgoing
38+
// messages. Once tokens (bytes) have been taken from the bucket,
39+
// they'll be refilled at this rate.
40+
DefaultMsgBytesPerSecond = 100 * 1_024
41+
42+
// assumedMsgSize is the assumed size of a message if we can't compute
43+
// its serialized size. This comes out to 1 KB.
44+
assumedMsgSize = 1_024
2845
)
2946

3047
var (
@@ -110,6 +127,15 @@ type SyncManagerCfg struct {
110127
// updates for a channel and returns true if the channel should be
111128
// considered a zombie based on these timestamps.
112129
IsStillZombieChannel func(time.Time, time.Time) bool
130+
131+
// AllotedMsgBytesPerSecond is the allotted bandwidth rate, expressed in
132+
// bytes/second that the gossip manager can consume. Once we exceed this
133+
// rate, message sending will block until we're below the rate.
134+
AllotedMsgBytesPerSecond uint64
135+
136+
// AllotedMsgBytesBurst is the amount of burst bytes we'll permit, if
137+
// we've exceeded the hard upper limit.
138+
AllotedMsgBytesBurst uint64
113139
}
114140

115141
// SyncManager is a subsystem of the gossiper that manages the gossip syncers
@@ -168,6 +194,12 @@ type SyncManager struct {
168194
// queries.
169195
gossipFilterSema chan struct{}
170196

197+
// rateLimiter dictates the frequency with which we will reply to gossip
198+
// queries from a peer. This is used to delay responses to peers to
199+
// prevent DOS vulnerabilities if they are spamming with an unreasonable
200+
// number of queries.
201+
rateLimiter *rate.Limiter
202+
171203
wg sync.WaitGroup
172204
quit chan struct{}
173205
}
@@ -180,8 +212,25 @@ func newSyncManager(cfg *SyncManagerCfg) *SyncManager {
180212
filterSema <- struct{}{}
181213
}
182214

215+
bytesPerSecond := cfg.AllotedMsgBytesPerSecond
216+
if bytesPerSecond == 0 {
217+
bytesPerSecond = DefaultMsgBytesPerSecond
218+
}
219+
220+
bytesBurst := cfg.AllotedMsgBytesBurst
221+
if bytesBurst == 0 {
222+
bytesBurst = DefaultMsgBytesBurst
223+
}
224+
225+
// We'll use this rate limiter to limit our total outbound bandwidth for
226+
// gossip queries peers.
227+
rateLimiter := rate.NewLimiter(
228+
rate.Limit(bytesPerSecond), int(bytesBurst),
229+
)
230+
183231
return &SyncManager{
184232
cfg: *cfg,
233+
rateLimiter: rateLimiter,
185234
newSyncers: make(chan *newSyncer),
186235
staleSyncers: make(chan *staleSyncer),
187236
activeSyncers: make(
@@ -494,6 +543,95 @@ func (m *SyncManager) isPinnedSyncer(s *GossipSyncer) bool {
494543
return isPinnedSyncer
495544
}
496545

546+
// deriveRateLimitReservation will take the current message and derive a
547+
// reservation that can be used to wait on the rate limiter.
548+
func (m *SyncManager) deriveRateLimitReservation(msg lnwire.Message,
549+
) (*rate.Reservation, error) {
550+
551+
var (
552+
msgSize uint32
553+
err error
554+
)
555+
556+
// Figure out the serialized size of the message. If we can't easily
557+
// compute it, then we'll used the assumed msg size.
558+
if sMsg, ok := msg.(lnwire.SizeableMessage); ok {
559+
msgSize, err = sMsg.SerializedSize()
560+
if err != nil {
561+
return nil, err
562+
}
563+
} else {
564+
log.Warnf("Unable to compute serialized size of %T", msg)
565+
566+
msgSize = assumedMsgSize
567+
}
568+
569+
return m.rateLimiter.ReserveN(time.Now(), int(msgSize)), nil
570+
}
571+
572+
// waitMsgDelay takes a delay, and waits until it has finished.
573+
func (m *SyncManager) waitMsgDelay(ctx context.Context, peerPub [33]byte,
574+
limitReservation *rate.Reservation) error {
575+
576+
// If we've already replied a handful of times, we will start to delay
577+
// responses back to the remote peer. This can help prevent DOS attacks
578+
// where the remote peer spams us endlessly.
579+
//
580+
// We skip checking for reservation.OK() here, as during config
581+
// validation, we ensure that the burst is enough for a single message
582+
// to be sent.
583+
delay := limitReservation.Delay()
584+
if delay > 0 {
585+
log.Infof("GossipSyncer(%x): rate limiting gossip replies, "+
586+
"responding in %s", peerPub, delay)
587+
588+
select {
589+
case <-time.After(delay):
590+
591+
case <-ctx.Done():
592+
limitReservation.Cancel()
593+
594+
return ErrGossipSyncerExiting
595+
596+
case <-m.quit:
597+
limitReservation.Cancel()
598+
599+
return ErrGossipSyncerExiting
600+
}
601+
}
602+
603+
return nil
604+
}
605+
606+
// maybeRateLimitMsg takes a message, and may wait a period of time to rate
607+
// limit the msg.
608+
func (m *SyncManager) maybeRateLimitMsg(ctx context.Context, peerPub [33]byte,
609+
msg lnwire.Message) error {
610+
611+
delay, err := m.deriveRateLimitReservation(msg)
612+
if err != nil {
613+
return nil
614+
}
615+
616+
return m.waitMsgDelay(ctx, peerPub, delay)
617+
}
618+
619+
// sendMessages sends a set of messages to the remote peer.
620+
func (m *SyncManager) sendMessages(ctx context.Context, sync bool,
621+
peer lnpeer.Peer, nodeID route.Vertex, msgs ...lnwire.Message) error {
622+
623+
for _, msg := range msgs {
624+
if err := m.maybeRateLimitMsg(ctx, nodeID, msg); err != nil {
625+
return err
626+
}
627+
if err := peer.SendMessageLazy(sync, msg); err != nil {
628+
return err
629+
}
630+
}
631+
632+
return nil
633+
}
634+
497635
// createGossipSyncer creates the GossipSyncer for a newly connected peer.
498636
func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer {
499637
nodeID := route.Vertex(peer.PubKey())
@@ -507,20 +645,22 @@ func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer {
507645
encodingType: encoding,
508646
chunkSize: encodingTypeToChunkSize[encoding],
509647
batchSize: requestBatchSize,
510-
sendToPeer: func(msgs ...lnwire.Message) error {
511-
return peer.SendMessageLazy(false, msgs...)
648+
sendToPeer: func(ctx context.Context,
649+
msgs ...lnwire.Message) error {
650+
651+
return m.sendMessages(ctx, false, peer, nodeID, msgs...)
512652
},
513-
sendToPeerSync: func(msgs ...lnwire.Message) error {
514-
return peer.SendMessageLazy(true, msgs...)
653+
sendToPeerSync: func(ctx context.Context,
654+
msgs ...lnwire.Message) error {
655+
656+
return m.sendMessages(ctx, true, peer, nodeID, msgs...)
515657
},
516-
ignoreHistoricalFilters: m.cfg.IgnoreHistoricalFilters,
517-
maxUndelayedQueryReplies: DefaultMaxUndelayedQueryReplies,
518-
delayedQueryReplyInterval: DefaultDelayedQueryReplyInterval,
519-
bestHeight: m.cfg.BestHeight,
520-
markGraphSynced: m.markGraphSynced,
521-
maxQueryChanRangeReplies: maxQueryChanRangeReplies,
522-
noTimestampQueryOption: m.cfg.NoTimestampQueries,
523-
isStillZombieChannel: m.cfg.IsStillZombieChannel,
658+
ignoreHistoricalFilters: m.cfg.IgnoreHistoricalFilters,
659+
bestHeight: m.cfg.BestHeight,
660+
markGraphSynced: m.markGraphSynced,
661+
maxQueryChanRangeReplies: maxQueryChanRangeReplies,
662+
noTimestampQueryOption: m.cfg.NoTimestampQueries,
663+
isStillZombieChannel: m.cfg.IsStillZombieChannel,
524664
}, m.gossipFilterSema)
525665

526666
// Gossip syncers are initialized by default in a PassiveSync type

0 commit comments

Comments
 (0)