/ accessman.go
accessman.go
  1  package lnd
  2  
  3  import (
  4  	"context"
  5  	"fmt"
  6  	"maps"
  7  	"sync"
  8  
  9  	"github.com/btcsuite/btcd/btcec/v2"
 10  	"github.com/btcsuite/btclog/v2"
 11  	"github.com/lightningnetwork/lnd/channeldb"
 12  	"github.com/lightningnetwork/lnd/lnutils"
 13  )
 14  
 15  // accessMan is responsible for managing the server's access permissions.
 16  type accessMan struct {
 17  	cfg *accessManConfig
 18  
 19  	// banScoreMtx is used for the server's ban tracking. If the server
 20  	// mutex is also going to be locked, ensure that this is locked after
 21  	// the server mutex.
 22  	banScoreMtx sync.RWMutex
 23  
 24  	// peerChanInfo is a mapping from remote public key to {bool, uint64}
 25  	// where the bool indicates that we have an open/closed channel with the
 26  	// peer and where the uint64 indicates the number of pending-open
 27  	// channels we currently have with them. This mapping will be used to
 28  	// determine access permissions for the peer. The map key is the
 29  	// string-version of the serialized public key.
 30  	//
 31  	// NOTE: This MUST be accessed with the banScoreMtx held.
 32  	peerChanInfo map[string]channeldb.ChanCount
 33  
 34  	// peerScores stores each connected peer's access status. The map key
 35  	// is the string-version of the serialized public key.
 36  	//
 37  	// NOTE: This MUST be accessed with the banScoreMtx held.
 38  	//
 39  	// TODO(yy): unify `peerScores` and `peerChanInfo` - there's no need to
 40  	// create two maps tracking essentially the same info. `numRestricted`
 41  	// can also be derived from `peerChanInfo`.
 42  	peerScores map[string]peerSlotStatus
 43  
 44  	// numRestricted tracks the number of peers with restricted access in
 45  	// peerScores. This MUST be accessed with the banScoreMtx held.
 46  	numRestricted int64
 47  }
 48  
 49  type accessManConfig struct {
 50  	// initAccessPerms checks the channeldb for initial access permissions
 51  	// and then populates the peerChanInfo and peerScores maps.
 52  	initAccessPerms func() (map[string]channeldb.ChanCount, error)
 53  
 54  	// shouldDisconnect determines whether we should disconnect a peer or
 55  	// not.
 56  	shouldDisconnect func(*btcec.PublicKey) (bool, error)
 57  
 58  	// maxRestrictedSlots is the number of restricted slots we'll allocate.
 59  	maxRestrictedSlots int64
 60  }
 61  
 62  func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
 63  	a := &accessMan{
 64  		cfg:          cfg,
 65  		peerChanInfo: make(map[string]channeldb.ChanCount),
 66  		peerScores:   make(map[string]peerSlotStatus),
 67  	}
 68  
 69  	counts, err := a.cfg.initAccessPerms()
 70  	if err != nil {
 71  		return nil, err
 72  	}
 73  
 74  	// We'll populate the server's peerChanInfo map with the counts fetched
 75  	// via initAccessPerms. Also note that we haven't yet connected to the
 76  	// peers.
 77  	maps.Copy(a.peerChanInfo, counts)
 78  
 79  	acsmLog.Info("Access Manager initialized")
 80  
 81  	return a, nil
 82  }
 83  
 84  // hasPeer checks whether a given peer already exists in the internal maps.
 85  func (a *accessMan) hasPeer(ctx context.Context,
 86  	pub string) (peerAccessStatus, bool) {
 87  
 88  	// Lock banScoreMtx for reading so that we can read the banning maps
 89  	// below.
 90  	a.banScoreMtx.RLock()
 91  	defer a.banScoreMtx.RUnlock()
 92  
 93  	count, found := a.peerChanInfo[pub]
 94  	if found {
 95  		if count.HasOpenOrClosedChan {
 96  			acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
 97  				"assigning protected access")
 98  
 99  			// Exit early if the peer is no longer restricted.
100  			return peerStatusProtected, true
101  		}
102  
103  		if count.PendingOpenCount != 0 {
104  			acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
105  				"assigning temporary access")
106  
107  			// Exit early if the peer is no longer restricted.
108  			return peerStatusTemporary, true
109  		}
110  
111  		return peerStatusRestricted, true
112  	}
113  
114  	// Check if the peer is found in the scores map.
115  	status, found := a.peerScores[pub]
116  	if found {
117  		acsmLog.DebugS(ctx, "Peer already has access", "access",
118  			status.state)
119  
120  		return status.state, true
121  	}
122  
123  	return peerStatusRestricted, false
124  }
125  
126  // assignPeerPerms assigns a new peer its permissions. This does not track the
127  // access in the maps. This is intentional.
128  func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
129  	peerAccessStatus, error) {
130  
131  	ctx := btclog.WithCtx(
132  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
133  	)
134  
135  	peerMapKey := string(remotePub.SerializeCompressed())
136  
137  	acsmLog.DebugS(ctx, "Assigning permissions")
138  
139  	// Default is restricted unless the below filters say otherwise.
140  	access, peerExist := a.hasPeer(ctx, peerMapKey)
141  
142  	// Exit early if the peer is not restricted.
143  	if access != peerStatusRestricted {
144  		return access, nil
145  	}
146  
147  	// If we are here, it means the peer has peerStatusRestricted.
148  	//
149  	// Check whether this peer is banned.
150  	shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
151  	if err != nil {
152  		acsmLog.ErrorS(ctx, "Error checking disconnect status", err)
153  
154  		// Access is restricted here.
155  		return access, err
156  	}
157  
158  	if shouldDisconnect {
159  		acsmLog.WarnS(ctx, "Peer is banned, assigning restricted access",
160  			ErrGossiperBan)
161  
162  		// Access is restricted here.
163  		return access, ErrGossiperBan
164  	}
165  
166  	// If we've reached this point and access hasn't changed from
167  	// restricted, then we need to check if we even have a slot for this
168  	// peer.
169  	acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access")
170  
171  	// If this is an existing peer, there's no need to check for slot limit.
172  	if peerExist {
173  		acsmLog.DebugS(ctx, "Skipped slot check for existing peer")
174  		return access, nil
175  	}
176  
177  	a.banScoreMtx.RLock()
178  	defer a.banScoreMtx.RUnlock()
179  
180  	if a.numRestricted >= a.cfg.maxRestrictedSlots {
181  		acsmLog.WarnS(ctx, "No more restricted slots available, "+
182  			"denying peer", ErrNoMoreRestrictedAccessSlots,
183  			"num_restricted", a.numRestricted, "max_restricted",
184  			a.cfg.maxRestrictedSlots)
185  
186  		return access, ErrNoMoreRestrictedAccessSlots
187  	}
188  
189  	return access, nil
190  }
191  
192  // newPendingOpenChan is called after the pending-open channel has been
193  // committed to the database. This may transition a restricted-access peer to a
194  // temporary-access peer.
195  func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
196  	a.banScoreMtx.Lock()
197  	defer a.banScoreMtx.Unlock()
198  
199  	ctx := btclog.WithCtx(
200  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
201  	)
202  
203  	acsmLog.DebugS(ctx, "Processing new pending open channel")
204  
205  	peerMapKey := string(remotePub.SerializeCompressed())
206  
207  	// Fetch the peer's access status from peerScores.
208  	status, found := a.peerScores[peerMapKey]
209  	if !found {
210  		acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
211  
212  		// If we didn't find the peer, we'll return an error.
213  		return ErrNoPeerScore
214  	}
215  
216  	switch status.state {
217  	case peerStatusProtected:
218  		acsmLog.DebugS(ctx, "Peer already protected, no change")
219  
220  		// If this peer's access status is protected, we don't need to
221  		// do anything.
222  		return nil
223  
224  	case peerStatusTemporary:
225  		// If this peer's access status is temporary, we'll need to
226  		// update the peerChanInfo map. The peer's access status will
227  		// stay temporary.
228  		peerCount, found := a.peerChanInfo[peerMapKey]
229  		if !found {
230  			// Error if we did not find any info in peerChanInfo.
231  			acsmLog.ErrorS(ctx, "Pending peer info not found",
232  				ErrNoPendingPeerInfo)
233  
234  			return ErrNoPendingPeerInfo
235  		}
236  
237  		// Increment the pending channel amount.
238  		peerCount.PendingOpenCount += 1
239  		a.peerChanInfo[peerMapKey] = peerCount
240  
241  		acsmLog.DebugS(ctx, "Peer is temporary, incremented "+
242  			"pending count",
243  			"pending_count", peerCount.PendingOpenCount)
244  
245  	case peerStatusRestricted:
246  		// If the peer's access status is restricted, then we can
247  		// transition it to a temporary-access peer. We'll need to
248  		// update numRestricted and also peerScores. We'll also need to
249  		// update peerChanInfo.
250  		peerCount := channeldb.ChanCount{
251  			HasOpenOrClosedChan: false,
252  			PendingOpenCount:    1,
253  		}
254  
255  		a.peerChanInfo[peerMapKey] = peerCount
256  
257  		// A restricted-access slot has opened up.
258  		oldRestricted := a.numRestricted
259  		a.numRestricted -= 1
260  
261  		a.peerScores[peerMapKey] = peerSlotStatus{
262  			state: peerStatusTemporary,
263  		}
264  
265  		acsmLog.InfoS(ctx, "Peer transitioned restricted -> "+
266  			"temporary (pending open)",
267  			"old_restricted", oldRestricted,
268  			"new_restricted", a.numRestricted)
269  
270  	default:
271  		// This should not be possible.
272  		err := fmt.Errorf("invalid peer access status %v for %x",
273  			status.state, peerMapKey)
274  		acsmLog.ErrorS(ctx, "Invalid peer access status", err)
275  
276  		return err
277  	}
278  
279  	return nil
280  }
281  
282  // newPendingCloseChan is called when a pending-open channel prematurely closes
283  // before the funding transaction has confirmed. This potentially demotes a
284  // temporary-access peer to a restricted-access peer. If no restricted-access
285  // slots are available, the peer will be disconnected.
286  func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
287  	a.banScoreMtx.Lock()
288  	defer a.banScoreMtx.Unlock()
289  
290  	ctx := btclog.WithCtx(
291  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
292  	)
293  
294  	acsmLog.DebugS(ctx, "Processing pending channel close")
295  
296  	peerMapKey := string(remotePub.SerializeCompressed())
297  
298  	// Fetch the peer's access status from peerScores.
299  	status, found := a.peerScores[peerMapKey]
300  	if !found {
301  		acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
302  
303  		return ErrNoPeerScore
304  	}
305  
306  	switch status.state {
307  	case peerStatusProtected:
308  		// If this peer is protected, we don't do anything.
309  		acsmLog.DebugS(ctx, "Peer is protected, no change")
310  
311  		return nil
312  
313  	case peerStatusTemporary:
314  		// If this peer is temporary, we need to check if it will
315  		// revert to a restricted-access peer.
316  		peerCount, found := a.peerChanInfo[peerMapKey]
317  		if !found {
318  			acsmLog.ErrorS(ctx, "Pending peer info not found",
319  				ErrNoPendingPeerInfo)
320  
321  			// Error if we did not find any info in peerChanInfo.
322  			return ErrNoPendingPeerInfo
323  		}
324  
325  		currentNumPending := peerCount.PendingOpenCount - 1
326  
327  		acsmLog.DebugS(ctx, "Peer is temporary, decrementing "+
328  			"pending count",
329  			"pending_count", currentNumPending)
330  
331  		if currentNumPending == 0 {
332  			// Remove the entry from peerChanInfo.
333  			delete(a.peerChanInfo, peerMapKey)
334  
335  			// If this is the only pending-open channel for this
336  			// peer and it's getting removed, attempt to demote
337  			// this peer to a restricted peer.
338  			if a.numRestricted == a.cfg.maxRestrictedSlots {
339  				// There are no available restricted slots, so
340  				// we need to disconnect this peer. We leave
341  				// this up to the caller.
342  				acsmLog.WarnS(ctx, "Peer last pending "+
343  					"channel closed: ",
344  					ErrNoMoreRestrictedAccessSlots,
345  					"num_restricted", a.numRestricted,
346  					"max_restricted", a.cfg.maxRestrictedSlots)
347  
348  				return ErrNoMoreRestrictedAccessSlots
349  			}
350  
351  			// Otherwise, there is an available restricted-access
352  			// slot, so we can demote this peer.
353  			a.peerScores[peerMapKey] = peerSlotStatus{
354  				state: peerStatusRestricted,
355  			}
356  
357  			// Update numRestricted.
358  			oldRestricted := a.numRestricted
359  			a.numRestricted++
360  
361  			acsmLog.InfoS(ctx, "Peer transitioned "+
362  				"temporary -> restricted "+
363  				"(last pending closed)",
364  				"old_restricted", oldRestricted,
365  				"new_restricted", a.numRestricted)
366  
367  			return nil
368  		}
369  
370  		// Else, we don't need to demote this peer since it has other
371  		// pending-open channels with us.
372  		peerCount.PendingOpenCount = currentNumPending
373  		a.peerChanInfo[peerMapKey] = peerCount
374  
375  		acsmLog.DebugS(ctx, "Peer still has other pending channels",
376  			"pending_count", currentNumPending)
377  
378  		return nil
379  
380  	case peerStatusRestricted:
381  		// This should not be possible. This indicates an error.
382  		err := fmt.Errorf("invalid peer access state transition: "+
383  			"pending close for restricted peer %x", peerMapKey)
384  		acsmLog.ErrorS(ctx, "Invalid peer access state transition", err)
385  
386  		return err
387  
388  	default:
389  		// This should not be possible.
390  		err := fmt.Errorf("invalid peer access status %v for %x",
391  			status.state, peerMapKey)
392  		acsmLog.ErrorS(ctx, "Invalid peer access status", err)
393  
394  		return err
395  	}
396  }
397  
398  // newOpenChan is called when a pending-open channel becomes an open channel
399  // (i.e. the funding transaction has confirmed). If the remote peer is a
400  // temporary-access peer, it will be promoted to a protected-access peer.
401  func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
402  	a.banScoreMtx.Lock()
403  	defer a.banScoreMtx.Unlock()
404  
405  	ctx := btclog.WithCtx(
406  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
407  	)
408  
409  	acsmLog.DebugS(ctx, "Processing new open channel")
410  
411  	peerMapKey := string(remotePub.SerializeCompressed())
412  
413  	// Fetch the peer's access status from peerScores.
414  	status, found := a.peerScores[peerMapKey]
415  	if !found {
416  		// If we didn't find the peer, we'll return an error.
417  		acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
418  
419  		return ErrNoPeerScore
420  	}
421  
422  	switch status.state {
423  	case peerStatusProtected:
424  		acsmLog.DebugS(ctx, "Peer already protected, no change")
425  
426  		// If the peer's state is already protected, we don't need to do
427  		// anything more.
428  		return nil
429  
430  	case peerStatusTemporary:
431  		// If the peer's state is temporary, we'll upgrade the peer to
432  		// a protected peer.
433  		peerCount, found := a.peerChanInfo[peerMapKey]
434  		if !found {
435  			// Error if we did not find any info in peerChanInfo.
436  			acsmLog.ErrorS(ctx, "Pending peer info not found",
437  				ErrNoPendingPeerInfo)
438  
439  			return ErrNoPendingPeerInfo
440  		}
441  
442  		peerCount.HasOpenOrClosedChan = true
443  		peerCount.PendingOpenCount -= 1
444  
445  		a.peerChanInfo[peerMapKey] = peerCount
446  
447  		newStatus := peerSlotStatus{
448  			state: peerStatusProtected,
449  		}
450  		a.peerScores[peerMapKey] = newStatus
451  
452  		acsmLog.InfoS(ctx, "Peer transitioned temporary -> "+
453  			"protected (channel opened)")
454  
455  		return nil
456  
457  	case peerStatusRestricted:
458  		// This should not be possible. For the server to receive a
459  		// state-transition event via NewOpenChan, the server must have
460  		// previously granted this peer "temporary" access. This
461  		// temporary access would not have been revoked or downgraded
462  		// without `CloseChannel` being called with the pending
463  		// argument set to true. This means that an open-channel state
464  		// transition would be impossible. Therefore, we can return an
465  		// error.
466  		err := fmt.Errorf("invalid peer access status: new open "+
467  			"channel for restricted peer %x", peerMapKey)
468  
469  		acsmLog.ErrorS(ctx, "Invalid peer access status", err)
470  
471  		return err
472  
473  	default:
474  		// This should not be possible.
475  		err := fmt.Errorf("invalid peer access status %v for %x",
476  			status.state, peerMapKey)
477  
478  		acsmLog.ErrorS(ctx, "Invalid peer access status", err)
479  
480  		return err
481  	}
482  }
483  
484  // checkAcceptIncomingConn checks whether, given the remote's public hex-
485  // encoded key, we should not accept this incoming connection or immediately
486  // disconnect. This does not assign to the server's peerScores maps. This is
487  // just an inbound filter that the brontide listeners use.
488  //
489  // TODO(yy): We should also consider removing this `checkAcceptIncomingConn`
490  // check as a) it doesn't check for ban score; and b) we should, and already
491  // have this check when we handle incoming connection in `InboundPeerConnected`.
492  func (a *accessMan) checkAcceptIncomingConn(remotePub *btcec.PublicKey) (
493  	bool, error) {
494  
495  	ctx := btclog.WithCtx(
496  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
497  	)
498  
499  	peerMapKey := string(remotePub.SerializeCompressed())
500  
501  	acsmLog.TraceS(ctx, "Checking incoming connection ban score")
502  
503  	a.banScoreMtx.RLock()
504  	defer a.banScoreMtx.RUnlock()
505  
506  	_, found := a.peerChanInfo[peerMapKey]
507  
508  	// Exit early if found.
509  	if found {
510  		acsmLog.DebugS(ctx, "Peer found (protected/temporary), "+
511  			"accepting")
512  
513  		return true, nil
514  	}
515  
516  	_, found = a.peerScores[peerMapKey]
517  
518  	// Exit early if found.
519  	if found {
520  		acsmLog.DebugS(ctx, "Found existing peer, accepting")
521  
522  		return true, nil
523  	}
524  
525  	acsmLog.DebugS(ctx, "Peer not found in counts, checking restricted "+
526  		"slots")
527  
528  	// Check numRestricted to see if there is an available slot. In
529  	// the future, it's possible to add better heuristics.
530  	if a.numRestricted < a.cfg.maxRestrictedSlots {
531  		// There is an available slot.
532  		acsmLog.DebugS(ctx, "Restricted slot available, accepting ",
533  			"num_restricted", a.numRestricted, "max_restricted",
534  			a.cfg.maxRestrictedSlots)
535  
536  		return true, nil
537  	}
538  
539  	// If there are no slots left, then we reject this connection.
540  	acsmLog.WarnS(ctx, "No restricted slots available, rejecting ",
541  		ErrNoMoreRestrictedAccessSlots, "num_restricted",
542  		a.numRestricted, "max_restricted", a.cfg.maxRestrictedSlots)
543  
544  	return false, ErrNoMoreRestrictedAccessSlots
545  }
546  
547  // addPeerAccess tracks a peer's access in the maps. This should be called when
548  // the peer has fully connected.
549  func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
550  	access peerAccessStatus, inbound bool) {
551  
552  	ctx := btclog.WithCtx(
553  		context.TODO(), lnutils.LogPubKey("peer", remotePub),
554  	)
555  
556  	acsmLog.DebugS(ctx, "Adding peer access", "access", access)
557  
558  	// Add the remote public key to peerScores.
559  	a.banScoreMtx.Lock()
560  	defer a.banScoreMtx.Unlock()
561  
562  	peerMapKey := string(remotePub.SerializeCompressed())
563  
564  	// Exit early if this is an existing peer, which means it won't take
565  	// another slot.
566  	_, found := a.peerScores[peerMapKey]
567  	if found {
568  		acsmLog.DebugS(ctx, "Skipped taking restricted slot for "+
569  			"existing peer")
570  
571  		return
572  	}
573  
574  	a.peerScores[peerMapKey] = peerSlotStatus{state: access}
575  
576  	// Exit early if this is not a restricted peer.
577  	if access != peerStatusRestricted {
578  		acsmLog.DebugS(ctx, "Skipped taking restricted slot as peer "+
579  			"already has access", "access", access)
580  
581  		return
582  	}
583  
584  	// Increment numRestricted if this is an inbound connection.
585  	if inbound {
586  		oldRestricted := a.numRestricted
587  		a.numRestricted++
588  
589  		acsmLog.DebugS(ctx, "Incremented restricted slots",
590  			"old_restricted", oldRestricted,
591  			"new_restricted", a.numRestricted)
592  
593  		return
594  	}
595  
596  	// Otherwise, this is a newly created outbound connection. We won't
597  	// place any restriction on it, instead, we will do a hot upgrade here
598  	// to move it from restricted to temporary.
599  	peerCount := channeldb.ChanCount{
600  		HasOpenOrClosedChan: false,
601  		PendingOpenCount:    0,
602  	}
603  
604  	a.peerChanInfo[peerMapKey] = peerCount
605  	a.peerScores[peerMapKey] = peerSlotStatus{
606  		state: peerStatusTemporary,
607  	}
608  
609  	acsmLog.InfoS(ctx, "Upgraded outbound peer: restricted -> temporary")
610  }
611  
612  // removePeerAccess removes the peer's access from the maps. This should be
613  // called when the peer has been disconnected.
614  func (a *accessMan) removePeerAccess(ctx context.Context, peerPubKey string) {
615  	acsmLog.DebugS(ctx, "Removing access:")
616  
617  	a.banScoreMtx.Lock()
618  	defer a.banScoreMtx.Unlock()
619  
620  	status, found := a.peerScores[peerPubKey]
621  	if !found {
622  		acsmLog.InfoS(ctx, "Peer score not found during removal")
623  		return
624  	}
625  
626  	if status.state == peerStatusRestricted {
627  		// If the status is restricted, then we decrement from
628  		// numRestrictedSlots.
629  		oldRestricted := a.numRestricted
630  		a.numRestricted--
631  
632  		acsmLog.DebugS(ctx, "Decremented restricted slots",
633  			"old_restricted", oldRestricted,
634  			"new_restricted", a.numRestricted)
635  	}
636  
637  	acsmLog.TraceS(ctx, "Deleting from peerScores:")
638  
639  	delete(a.peerScores, peerPubKey)
640  
641  	// We now check whether this peer has channels with us or not.
642  	info, found := a.peerChanInfo[peerPubKey]
643  	if !found {
644  		acsmLog.DebugS(ctx, "Chan info not found during removal:")
645  		return
646  	}
647  
648  	// Exit early if the peer has channel(s) with us.
649  	if info.HasOpenOrClosedChan {
650  		acsmLog.DebugS(ctx, "Skipped removing peer with channels:")
651  		return
652  	}
653  
654  	// Skip removing the peer if it has pending open/close with us.
655  	if info.PendingOpenCount != 0 {
656  		acsmLog.DebugS(ctx, "Skipped removing peer with pending "+
657  			"channels:")
658  		return
659  	}
660  
661  	// Given this peer has no channels with us, we can now remove it.
662  	delete(a.peerChanInfo, peerPubKey)
663  	acsmLog.TraceS(ctx, "Removed peer from peerChanInfo:")
664  }