From a56e7122d60ec4cc196019a4988f7856f21ca972 Mon Sep 17 00:00:00 2001 From: Conner Fromknecht Date: Tue, 11 Sep 2018 00:07:20 -0700 Subject: [PATCH] discovery/syncer: use rate limiter for gossip queries This commit replaces the simplistic rate limiting technique added in 557cb6e2, to use the golang.org/x/time's rate limiter. This has the benefit of performing traffic shaping to meet a target maximum rate, and yet tolerate bursts. Bursts are expected during initial sync, though should become more rare afterwards. Performing traffic shaping with this mechanism should improve the ability of the gossip syncer to detect sustained bursts from the remote peer, and penalize them appropriately. This commit also modifies the default parameters to accept bursts of 10 queries, with a target rate of 1 reply every 5 seconds. --- discovery/syncer.go | 47 ++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/discovery/syncer.go b/discovery/syncer.go index dd353fde..ba47056e 100644 --- a/discovery/syncer.go +++ b/discovery/syncer.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lnwire" + "golang.org/x/time/rate" ) // syncerState is an enum that represents the current state of the @@ -56,12 +57,12 @@ const ( const ( // DefaultMaxUndelayedQueryReplies specifies how many gossip queries we // will respond to immediately before starting to delay responses. - DefaultMaxUndelayedQueryReplies = 5 + DefaultMaxUndelayedQueryReplies = 10 // DefaultDelayedQueryReplyInterval is the length of time we will wait // before responding to gossip queries after replying to // maxUndelayedQueryReplies queries. - DefaultDelayedQueryReplyInterval = 30 * time.Second + DefaultDelayedQueryReplyInterval = 5 * time.Second ) // String returns a human readable string describing the target syncerState. @@ -238,10 +239,11 @@ type gossipSyncer struct { cfg gossipSyncerCfg - // replyCount records how many query replies we've responded to. This is - // used to determine when to start delaying responses to peers to - // prevent DOS vulnerabilities. - replyCount int + // rateLimiter dictates the frequency with which we will reply to gossip + // queries from a peer. This is used to delay responses to peers to + // prevent DOS vulnerabilities if they are spamming with an unreasonable + // number of queries. + rateLimiter *rate.Limiter sync.Mutex @@ -259,15 +261,25 @@ func newGossiperSyncer(cfg gossipSyncerCfg) *gossipSyncer { } // If no parameter was specified for delayed query reply interval, set - // to the default of 30 seconds. + // to the default of 5 seconds. if cfg.delayedQueryReplyInterval <= 0 { cfg.delayedQueryReplyInterval = DefaultDelayedQueryReplyInterval } + // Construct a rate limiter that will govern how frequently we reply to + // gossip queries from this peer. The limiter will automatically adjust + // during periods of quiescence, and increase the reply interval under + // load. + interval := rate.Every(cfg.delayedQueryReplyInterval) + rateLimiter := rate.NewLimiter( + interval, cfg.maxUndelayedQueryReplies, + ) + return &gossipSyncer{ - cfg: cfg, - gossipMsgs: make(chan lnwire.Message, 100), - quit: make(chan struct{}), + cfg: cfg, + rateLimiter: rateLimiter, + gossipMsgs: make(chan lnwire.Message, 100), + quit: make(chan struct{}), } } @@ -629,23 +641,22 @@ func (g *gossipSyncer) genChanRangeQuery() (*lnwire.QueryChannelRange, error) { // replyPeerQueries is called in response to any query by the remote peer. // We'll examine our state and send back our best response. func (g *gossipSyncer) replyPeerQueries(msg lnwire.Message) error { + reservation := g.rateLimiter.Reserve() + delay := reservation.Delay() + // If we've already replied a handful of times, we will start to delay // responses back to the remote peer. This can help prevent DOS attacks // where the remote peer spams us endlessly. - switch { - case g.replyCount == g.cfg.maxUndelayedQueryReplies: - log.Infof("gossipSyncer(%x): entering delayed gossip replies", - g.peerPub[:]) - fallthrough + if delay > 0 { + log.Infof("gossipSyncer(%x): rate limiting gossip replies, ", + "responding in %s", g.peerPub[:], delay) - case g.replyCount > g.cfg.maxUndelayedQueryReplies: select { - case <-time.After(g.cfg.delayedQueryReplyInterval): + case <-time.After(delay): case <-g.quit: return ErrGossipSyncerExiting } } - g.replyCount++ switch msg := msg.(type) {