Merge pull request #9880 from yyforyongyu/accessman

Improve access control in peer connections
This commit is contained in:
Olaoluwa Osuntokun authored and GitHub committed 2025-06-20 15:26:24 -07:00
commit faa68a220e
15 files changed
+1104 -305

No files matched your search

+192 -87
View File
@@ -21,20 +21,24 @@ type accessMan struct {
// the server mutex. // the server mutex.
banScoreMtx sync.RWMutex banScoreMtx sync.RWMutex
// peerCounts is a mapping from remote public key to {bool, uint64} // peerChanInfo is a mapping from remote public key to {bool, uint64}
// where the bool indicates that we have an open/closed channel with // where the bool indicates that we have an open/closed channel with the
// the peer and where the uint64 indicates the number of pending-open // peer and where the uint64 indicates the number of pending-open
// channels we currently have with them. This mapping will be used to // channels we currently have with them. This mapping will be used to
// determine access permissions for the peer. The map key is the // determine access permissions for the peer. The map key is the
// string-version of the serialized public key. // string-version of the serialized public key.
// //
// NOTE: This MUST be accessed with the banScoreMtx held. // NOTE: This MUST be accessed with the banScoreMtx held.
peerCounts map[string]channeldb.ChanCount peerChanInfo map[string]channeldb.ChanCount
// peerScores stores each connected peer's access status. The map key // peerScores stores each connected peer's access status. The map key
// is the string-version of the serialized public key. // is the string-version of the serialized public key.
// //
// NOTE: This MUST be accessed with the banScoreMtx held. // NOTE: This MUST be accessed with the banScoreMtx held.
//
// TODO(yy): unify `peerScores` and `peerChanInfo` - there's no need to
// create two maps tracking essentially the same info. `numRestricted`
// can also be derived from `peerChanInfo`.
peerScores map[string]peerSlotStatus peerScores map[string]peerSlotStatus
// numRestricted tracks the number of peers with restricted access in // numRestricted tracks the number of peers with restricted access in
@@ -44,7 +48,7 @@ type accessMan struct {
type accessManConfig struct { type accessManConfig struct {
// initAccessPerms checks the channeldb for initial access permissions // initAccessPerms checks the channeldb for initial access permissions
// and then populates the peerCounts and peerScores maps. // and then populates the peerChanInfo and peerScores maps.
initAccessPerms func() (map[string]channeldb.ChanCount, error) initAccessPerms func() (map[string]channeldb.ChanCount, error)
// shouldDisconnect determines whether we should disconnect a peer or // shouldDisconnect determines whether we should disconnect a peer or
@@ -57,9 +61,9 @@ type accessManConfig struct {
func newAccessMan(cfg *accessManConfig) (*accessMan, error) { func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
a := &accessMan{ a := &accessMan{
cfg: cfg, cfg: cfg,
peerCounts: make(map[string]channeldb.ChanCount), peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus), peerScores: make(map[string]peerSlotStatus),
} }
counts, err := a.cfg.initAccessPerms() counts, err := a.cfg.initAccessPerms()
@@ -67,16 +71,58 @@ func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
return nil, err return nil, err
} }
// We'll populate the server's peerCounts map with the counts fetched // We'll populate the server's peerChanInfo map with the counts fetched
// via initAccessPerms. Also note that we haven't yet connected to the // via initAccessPerms. Also note that we haven't yet connected to the
// peers. // peers.
maps.Copy(a.peerCounts, counts) maps.Copy(a.peerChanInfo, counts)
acsmLog.Info("Access Manager initialized") acsmLog.Info("Access Manager initialized")
return a, nil return a, nil
} }
// hasPeer checks whether a given peer already exists in the internal maps.
func (a *accessMan) hasPeer(ctx context.Context,
pub string) (peerAccessStatus, bool) {
// Lock banScoreMtx for reading so that we can read the banning maps
// below.
a.banScoreMtx.RLock()
defer a.banScoreMtx.RUnlock()
count, found := a.peerChanInfo[pub]
if found {
if count.HasOpenOrClosedChan {
acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
"assigning protected access")
// Exit early if the peer is no longer restricted.
return peerStatusProtected, true
}
if count.PendingOpenCount != 0 {
acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
"assigning temporary access")
// Exit early if the peer is no longer restricted.
return peerStatusTemporary, true
}
return peerStatusRestricted, true
}
// Check if the peer is found in the scores map.
status, found := a.peerScores[pub]
if found {
acsmLog.DebugS(ctx, "Peer already has access", "access",
status.state)
return status.state, true
}
return peerStatusRestricted, false
}
// assignPeerPerms assigns a new peer its permissions. This does not track the // assignPeerPerms assigns a new peer its permissions. This does not track the
// access in the maps. This is intentional. // access in the maps. This is intentional.
func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) ( func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
@@ -91,31 +137,15 @@ func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
acsmLog.DebugS(ctx, "Assigning permissions") acsmLog.DebugS(ctx, "Assigning permissions")
// Default is restricted unless the below filters say otherwise. // Default is restricted unless the below filters say otherwise.
access := peerStatusRestricted access, peerExist := a.hasPeer(ctx, peerMapKey)
// Lock banScoreMtx for reading so that we can update the banning maps // Exit early if the peer is not restricted.
// below.
a.banScoreMtx.RLock()
if count, found := a.peerCounts[peerMapKey]; found {
if count.HasOpenOrClosedChan {
acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
"assigning protected access")
access = peerStatusProtected
} else if count.PendingOpenCount != 0 {
acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
"assigning temporary access")
access = peerStatusTemporary
}
}
a.banScoreMtx.RUnlock()
// Exit early if the peer status is no longer restricted.
if access != peerStatusRestricted { if access != peerStatusRestricted {
return access, nil return access, nil
} }
// If we are here, it means the peer has peerStatusRestricted.
//
// Check whether this peer is banned. // Check whether this peer is banned.
shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub) shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
if err != nil { if err != nil {
@@ -138,6 +168,12 @@ func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
// peer. // peer.
acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access") acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access")
// If this is an existing peer, there's no need to check for slot limit.
if peerExist {
acsmLog.DebugS(ctx, "Skipped slot check for existing peer")
return access, nil
}
a.banScoreMtx.RLock() a.banScoreMtx.RLock()
defer a.banScoreMtx.RUnlock() defer a.banScoreMtx.RUnlock()
@@ -187,11 +223,11 @@ func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
case peerStatusTemporary: case peerStatusTemporary:
// If this peer's access status is temporary, we'll need to // If this peer's access status is temporary, we'll need to
// update the peerCounts map. The peer's access status will stay // update the peerChanInfo map. The peer's access status will
// temporary. // stay temporary.
peerCount, found := a.peerCounts[peerMapKey] peerCount, found := a.peerChanInfo[peerMapKey]
if !found { if !found {
// Error if we did not find any info in peerCounts. // Error if we did not find any info in peerChanInfo.
acsmLog.ErrorS(ctx, "Pending peer info not found", acsmLog.ErrorS(ctx, "Pending peer info not found",
ErrNoPendingPeerInfo) ErrNoPendingPeerInfo)
@@ -200,7 +236,7 @@ func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
// Increment the pending channel amount. // Increment the pending channel amount.
peerCount.PendingOpenCount += 1 peerCount.PendingOpenCount += 1
a.peerCounts[peerMapKey] = peerCount a.peerChanInfo[peerMapKey] = peerCount
acsmLog.DebugS(ctx, "Peer is temporary, incremented "+ acsmLog.DebugS(ctx, "Peer is temporary, incremented "+
"pending count", "pending count",
@@ -210,13 +246,13 @@ func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
// If the peer's access status is restricted, then we can // If the peer's access status is restricted, then we can
// transition it to a temporary-access peer. We'll need to // transition it to a temporary-access peer. We'll need to
// update numRestricted and also peerScores. We'll also need to // update numRestricted and also peerScores. We'll also need to
// update peerCounts. // update peerChanInfo.
peerCount := channeldb.ChanCount{ peerCount := channeldb.ChanCount{
HasOpenOrClosedChan: false, HasOpenOrClosedChan: false,
PendingOpenCount: 1, PendingOpenCount: 1,
} }
a.peerCounts[peerMapKey] = peerCount a.peerChanInfo[peerMapKey] = peerCount
// A restricted-access slot has opened up. // A restricted-access slot has opened up.
oldRestricted := a.numRestricted oldRestricted := a.numRestricted
@@ -277,12 +313,12 @@ func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
case peerStatusTemporary: case peerStatusTemporary:
// If this peer is temporary, we need to check if it will // If this peer is temporary, we need to check if it will
// revert to a restricted-access peer. // revert to a restricted-access peer.
peerCount, found := a.peerCounts[peerMapKey] peerCount, found := a.peerChanInfo[peerMapKey]
if !found { if !found {
acsmLog.ErrorS(ctx, "Pending peer info not found", acsmLog.ErrorS(ctx, "Pending peer info not found",
ErrNoPendingPeerInfo) ErrNoPendingPeerInfo)
// Error if we did not find any info in peerCounts. // Error if we did not find any info in peerChanInfo.
return ErrNoPendingPeerInfo return ErrNoPendingPeerInfo
} }
@@ -293,8 +329,8 @@ func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
"pending_count", currentNumPending) "pending_count", currentNumPending)
if currentNumPending == 0 { if currentNumPending == 0 {
// Remove the entry from peerCounts. // Remove the entry from peerChanInfo.
delete(a.peerCounts, peerMapKey) delete(a.peerChanInfo, peerMapKey)
// If this is the only pending-open channel for this // If this is the only pending-open channel for this
// peer and it's getting removed, attempt to demote // peer and it's getting removed, attempt to demote
@@ -334,7 +370,7 @@ func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
// Else, we don't need to demote this peer since it has other // Else, we don't need to demote this peer since it has other
// pending-open channels with us. // pending-open channels with us.
peerCount.PendingOpenCount = currentNumPending peerCount.PendingOpenCount = currentNumPending
a.peerCounts[peerMapKey] = peerCount a.peerChanInfo[peerMapKey] = peerCount
acsmLog.DebugS(ctx, "Peer still has other pending channels", acsmLog.DebugS(ctx, "Peer still has other pending channels",
"pending_count", currentNumPending) "pending_count", currentNumPending)
@@ -394,9 +430,9 @@ func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
case peerStatusTemporary: case peerStatusTemporary:
// If the peer's state is temporary, we'll upgrade the peer to // If the peer's state is temporary, we'll upgrade the peer to
// a protected peer. // a protected peer.
peerCount, found := a.peerCounts[peerMapKey] peerCount, found := a.peerChanInfo[peerMapKey]
if !found { if !found {
// Error if we did not find any info in peerCounts. // Error if we did not find any info in peerChanInfo.
acsmLog.ErrorS(ctx, "Pending peer info not found", acsmLog.ErrorS(ctx, "Pending peer info not found",
ErrNoPendingPeerInfo) ErrNoPendingPeerInfo)
@@ -404,7 +440,9 @@ func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
} }
peerCount.HasOpenOrClosedChan = true peerCount.HasOpenOrClosedChan = true
a.peerCounts[peerMapKey] = peerCount peerCount.PendingOpenCount -= 1
a.peerChanInfo[peerMapKey] = peerCount
newStatus := peerSlotStatus{ newStatus := peerSlotStatus{
state: peerStatusProtected, state: peerStatusProtected,
@@ -443,11 +481,15 @@ func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
} }
} }
// checkIncomingConnBanScore checks whether, given the remote's public hex- // checkAcceptIncomingConn checks whether, given the remote's public hex-
// encoded key, we should not accept this incoming connection or immediately // encoded key, we should not accept this incoming connection or immediately
// disconnect. This does not assign to the server's peerScores maps. This is // disconnect. This does not assign to the server's peerScores maps. This is
// just an inbound filter that the brontide listeners use. // just an inbound filter that the brontide listeners use.
func (a *accessMan) checkIncomingConnBanScore(remotePub *btcec.PublicKey) ( //
// TODO(yy): We should also consider removing this `checkAcceptIncomingConn`
// check as a) it doesn't check for ban score; and b) we should, and already
// have this check when we handle incoming connection in `InboundPeerConnected`.
func (a *accessMan) checkAcceptIncomingConn(remotePub *btcec.PublicKey) (
bool, error) { bool, error) {
ctx := btclog.WithCtx( ctx := btclog.WithCtx(
@@ -461,42 +503,51 @@ func (a *accessMan) checkIncomingConnBanScore(remotePub *btcec.PublicKey) (
a.banScoreMtx.RLock() a.banScoreMtx.RLock()
defer a.banScoreMtx.RUnlock() defer a.banScoreMtx.RUnlock()
if _, found := a.peerCounts[peerMapKey]; !found { _, found := a.peerChanInfo[peerMapKey]
acsmLog.DebugS(ctx, "Peer not found in counts, "+
"checking restricted slots")
// Check numRestricted to see if there is an available slot. In // Exit early if found.
// the future, it's possible to add better heuristics. if found {
if a.numRestricted < a.cfg.maxRestrictedSlots { acsmLog.DebugS(ctx, "Peer found (protected/temporary), "+
// There is an available slot. "accepting")
acsmLog.DebugS(ctx, "Restricted slot available, "+
"accepting",
"num_restricted", a.numRestricted,
"max_restricted", a.cfg.maxRestrictedSlots)
return true, nil return true, nil
}
// If there are no slots left, then we reject this connection.
acsmLog.WarnS(ctx, "No restricted slots available, "+
"rejecting",
ErrNoMoreRestrictedAccessSlots,
"num_restricted", a.numRestricted,
"max_restricted", a.cfg.maxRestrictedSlots)
return false, ErrNoMoreRestrictedAccessSlots
} }
// Else, the peer is either protected or temporary. _, found = a.peerScores[peerMapKey]
acsmLog.DebugS(ctx, "Peer found (protected/temporary), accepting")
return true, nil // Exit early if found.
if found {
acsmLog.DebugS(ctx, "Found existing peer, accepting")
return true, nil
}
acsmLog.DebugS(ctx, "Peer not found in counts, checking restricted "+
"slots")
// Check numRestricted to see if there is an available slot. In
// the future, it's possible to add better heuristics.
if a.numRestricted < a.cfg.maxRestrictedSlots {
// There is an available slot.
acsmLog.DebugS(ctx, "Restricted slot available, accepting ",
"num_restricted", a.numRestricted, "max_restricted",
a.cfg.maxRestrictedSlots)
return true, nil
}
// If there are no slots left, then we reject this connection.
acsmLog.WarnS(ctx, "No restricted slots available, rejecting ",
ErrNoMoreRestrictedAccessSlots, "num_restricted",
a.numRestricted, "max_restricted", a.cfg.maxRestrictedSlots)
return false, ErrNoMoreRestrictedAccessSlots
} }
// addPeerAccess tracks a peer's access in the maps. This should be called when // addPeerAccess tracks a peer's access in the maps. This should be called when
// the peer has fully connected. // the peer has fully connected.
func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey, func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
access peerAccessStatus) { access peerAccessStatus, inbound bool) {
ctx := btclog.WithCtx( ctx := btclog.WithCtx(
context.TODO(), lnutils.LogPubKey("peer", remotePub), context.TODO(), lnutils.LogPubKey("peer", remotePub),
@@ -510,34 +561,64 @@ func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
peerMapKey := string(remotePub.SerializeCompressed()) peerMapKey := string(remotePub.SerializeCompressed())
// Exit early if this is an existing peer, which means it won't take
// another slot.
_, found := a.peerScores[peerMapKey]
if found {
acsmLog.DebugS(ctx, "Skipped taking restricted slot for "+
"existing peer")
return
}
a.peerScores[peerMapKey] = peerSlotStatus{state: access} a.peerScores[peerMapKey] = peerSlotStatus{state: access}
// Increment numRestricted. // Exit early if this is not a restricted peer.
if access == peerStatusRestricted { if access != peerStatusRestricted {
acsmLog.DebugS(ctx, "Skipped taking restricted slot as peer "+
"already has access", "access", access)
return
}
// Increment numRestricted if this is an inbound connection.
if inbound {
oldRestricted := a.numRestricted oldRestricted := a.numRestricted
a.numRestricted++ a.numRestricted++
acsmLog.DebugS(ctx, "Incremented restricted slots", acsmLog.DebugS(ctx, "Incremented restricted slots",
"old_restricted", oldRestricted, "old_restricted", oldRestricted,
"new_restricted", a.numRestricted) "new_restricted", a.numRestricted)
return
} }
// Otherwise, this is a newly created outbound connection. We won't
// place any restriction on it, instead, we will do a hot upgrade here
// to move it from restricted to temporary.
peerCount := channeldb.ChanCount{
HasOpenOrClosedChan: false,
PendingOpenCount: 0,
}
a.peerChanInfo[peerMapKey] = peerCount
a.peerScores[peerMapKey] = peerSlotStatus{
state: peerStatusTemporary,
}
acsmLog.InfoS(ctx, "Upgraded outbound peer: restricted -> temporary")
} }
// removePeerAccess removes the peer's access from the maps. This should be // removePeerAccess removes the peer's access from the maps. This should be
// called when the peer has been disconnected. // called when the peer has been disconnected.
func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) { func (a *accessMan) removePeerAccess(peerPubKey string) {
ctx := btclog.WithCtx(context.TODO(), "peer", peerPubKey)
acsmLog.DebugS(ctx, "Removing access:")
a.banScoreMtx.Lock() a.banScoreMtx.Lock()
defer a.banScoreMtx.Unlock() defer a.banScoreMtx.Unlock()
ctx := btclog.WithCtx( status, found := a.peerScores[peerPubKey]
context.TODO(), lnutils.LogPubKey("peer", remotePub),
)
acsmLog.DebugS(ctx, "Removing peer access")
peerMapKey := string(remotePub.SerializeCompressed())
status, found := a.peerScores[peerMapKey]
if !found { if !found {
acsmLog.InfoS(ctx, "Peer score not found during removal") acsmLog.InfoS(ctx, "Peer score not found during removal")
return return
@@ -554,7 +635,31 @@ func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) {
"new_restricted", a.numRestricted) "new_restricted", a.numRestricted)
} }
acsmLog.TraceS(ctx, "Deleting peer from peerScores") acsmLog.TraceS(ctx, "Deleting from peerScores:")
delete(a.peerScores, peerMapKey) delete(a.peerScores, peerPubKey)
// We now check whether this peer has channels with us or not.
info, found := a.peerChanInfo[peerPubKey]
if !found {
acsmLog.DebugS(ctx, "Chan info not found during removal:")
return
}
// Exit early if the peer has channel(s) with us.
if info.HasOpenOrClosedChan {
acsmLog.DebugS(ctx, "Skipped removing peer with channels:")
return
}
// Skip removing the peer if it has pending open/close with us.
if info.PendingOpenCount != 0 {
acsmLog.DebugS(ctx, "Skipped removing peer with pending "+
"channels:")
return
}
// Given this peer has no channels with us, we can now remove it.
delete(a.peerChanInfo, peerPubKey)
acsmLog.TraceS(ctx, "Removed peer from peerChanInfo:")
} }
+434 -17
View File
@@ -1,6 +1,7 @@
package lnd package lnd
import ( import (
"context"
"testing" "testing"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@@ -15,7 +16,7 @@ func assertInboundConnection(t *testing.T, a *accessMan,
remotePubSer := string(remotePub.SerializeCompressed()) remotePubSer := string(remotePub.SerializeCompressed())
isSlotAvailable, err := a.checkIncomingConnBanScore(remotePub) isSlotAvailable, err := a.checkAcceptIncomingConn(remotePub)
require.NoError(t, err) require.NoError(t, err)
require.True(t, isSlotAvailable) require.True(t, isSlotAvailable)
@@ -23,7 +24,7 @@ func assertInboundConnection(t *testing.T, a *accessMan,
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, status, peerAccess) require.Equal(t, status, peerAccess)
a.addPeerAccess(remotePub, peerAccess) a.addPeerAccess(remotePub, peerAccess, true)
peerScore, ok := a.peerScores[remotePubSer] peerScore, ok := a.peerScores[remotePubSer]
require.True(t, ok) require.True(t, ok)
require.Equal(t, status, peerScore.state) require.Equal(t, status, peerScore.state)
@@ -63,18 +64,25 @@ func TestAccessManRestrictedSlots(t *testing.T) {
peerKey3 := peerPriv3.PubKey() peerKey3 := peerPriv3.PubKey()
peerKeySer3 := string(peerKey3.SerializeCompressed()) peerKeySer3 := string(peerKey3.SerializeCompressed())
var (
peer1PendingCount = 0
peer2PendingCount = 1
peer3PendingCount = 1
)
initPerms := func() (map[string]channeldb.ChanCount, error) { initPerms := func() (map[string]channeldb.ChanCount, error) {
return map[string]channeldb.ChanCount{ return map[string]channeldb.ChanCount{
peerKeySer1: { peerKeySer1: {
HasOpenOrClosedChan: true, HasOpenOrClosedChan: true,
PendingOpenCount: uint64(peer1PendingCount),
}, },
peerKeySer2: { peerKeySer2: {
HasOpenOrClosedChan: true, HasOpenOrClosedChan: true,
PendingOpenCount: 1, PendingOpenCount: uint64(peer2PendingCount),
}, },
peerKeySer3: { peerKeySer3: {
HasOpenOrClosedChan: false, HasOpenOrClosedChan: false,
PendingOpenCount: 1, PendingOpenCount: uint64(peer3PendingCount),
}, },
}, nil }, nil
} }
@@ -92,25 +100,25 @@ func TestAccessManRestrictedSlots(t *testing.T) {
a, err := newAccessMan(cfg) a, err := newAccessMan(cfg)
require.NoError(t, err) require.NoError(t, err)
// Check that the peerCounts map is correctly populated with three // Check that the peerChanInfo map is correctly populated with three
// peers. // peers.
require.Equal(t, 0, int(a.numRestricted)) require.Equal(t, 0, int(a.numRestricted))
require.Equal(t, 3, len(a.peerCounts)) require.Equal(t, 3, len(a.peerChanInfo))
peerCount1, ok := a.peerCounts[peerKeySer1] peerCount1, ok := a.peerChanInfo[peerKeySer1]
require.True(t, ok) require.True(t, ok)
require.True(t, peerCount1.HasOpenOrClosedChan) require.True(t, peerCount1.HasOpenOrClosedChan)
require.Equal(t, 0, int(peerCount1.PendingOpenCount)) require.Equal(t, peer1PendingCount, int(peerCount1.PendingOpenCount))
peerCount2, ok := a.peerCounts[peerKeySer2] peerCount2, ok := a.peerChanInfo[peerKeySer2]
require.True(t, ok) require.True(t, ok)
require.True(t, peerCount2.HasOpenOrClosedChan) require.True(t, peerCount2.HasOpenOrClosedChan)
require.Equal(t, 1, int(peerCount2.PendingOpenCount)) require.Equal(t, peer2PendingCount, int(peerCount2.PendingOpenCount))
peerCount3, ok := a.peerCounts[peerKeySer3] peerCount3, ok := a.peerChanInfo[peerKeySer3]
require.True(t, ok) require.True(t, ok)
require.False(t, peerCount3.HasOpenOrClosedChan) require.False(t, peerCount3.HasOpenOrClosedChan)
require.Equal(t, 1, int(peerCount3.PendingOpenCount)) require.Equal(t, peer3PendingCount, int(peerCount3.PendingOpenCount))
// We'll now start to connect the peers. We'll add a new fourth peer // We'll now start to connect the peers. We'll add a new fourth peer
// that will take up the restricted slot. The first three peers should // that will take up the restricted slot. The first three peers should
@@ -120,7 +128,7 @@ func TestAccessManRestrictedSlots(t *testing.T) {
peerKey4 := peerPriv4.PubKey() peerKey4 := peerPriv4.PubKey()
// Follow the normal process of an incoming connection. We check if we // Follow the normal process of an incoming connection. We check if we
// can accommodate this peer in checkIncomingConnBanScore and then we // can accommodate this peer in checkAcceptIncomingConn and then we
// assign its access permissions and then insert into the map. // assign its access permissions and then insert into the map.
assertInboundConnection(t, a, peerKey4, peerStatusRestricted) assertInboundConnection(t, a, peerKey4, peerStatusRestricted)
@@ -134,11 +142,26 @@ func TestAccessManRestrictedSlots(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assertAccessState(t, a, peerKey4, peerStatusTemporary) assertAccessState(t, a, peerKey4, peerStatusTemporary)
// Assert that accessman's internal state is updated with peer4. We
// expect this new peer to have 1 pending open count.
peerCount4, ok := a.peerChanInfo[string(peerKey4.SerializeCompressed())]
require.True(t, ok)
require.False(t, peerCount4.HasOpenOrClosedChan)
require.Equal(t, 1, int(peerCount4.PendingOpenCount))
// Check that an open channel promotes the temporary peer. // Check that an open channel promotes the temporary peer.
err = a.newOpenChan(peerKey3) err = a.newOpenChan(peerKey3)
require.NoError(t, err) require.NoError(t, err)
assertAccessState(t, a, peerKey3, peerStatusProtected) assertAccessState(t, a, peerKey3, peerStatusProtected)
// Assert that accessman's internal state is updated with peer3. We
// expect this existing peer to decrement its pending open count and the
// flag `HasOpenOrClosedChan` should be true.
peerCount3, ok = a.peerChanInfo[peerKeySer3]
require.True(t, ok)
require.True(t, peerCount3.HasOpenOrClosedChan)
require.Equal(t, peer3PendingCount-1, int(peerCount3.PendingOpenCount))
// We should be able to accommodate a new peer. // We should be able to accommodate a new peer.
peerPriv5, err := btcec.NewPrivateKey() peerPriv5, err := btcec.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
@@ -150,6 +173,10 @@ func TestAccessManRestrictedSlots(t *testing.T) {
// peer. // peer.
err = a.newPendingCloseChan(peerKey4) err = a.newPendingCloseChan(peerKey4)
require.ErrorIs(t, err, ErrNoMoreRestrictedAccessSlots) require.ErrorIs(t, err, ErrNoMoreRestrictedAccessSlots)
// Assert that peer4 is removed.
_, ok = a.peerChanInfo[string(peerKey4.SerializeCompressed())]
require.False(t, ok)
} }
// TestAssignPeerPerms asserts that the peer's access status is correctly // TestAssignPeerPerms asserts that the peer's access status is correctly
@@ -250,9 +277,8 @@ func TestAssignPeerPerms(t *testing.T) {
expectedErr: ErrGossiperBan, expectedErr: ErrGossiperBan,
}, },
// peer6 has no channel with us, and we expect it to have a // peer6 has no channel with us, and we expect it to have a
// restricted status. We also expect the error // restricted status. Since this peer is seen, we don't expect
// `ErrNoMoreRestrictedAccessSlots` to be returned given // the error `ErrNoMoreRestrictedAccessSlots` to be returned.
// we only allow 1 restricted peer in this test.
{ {
name: "peer with no channels and restricted", name: "peer with no channels and restricted",
peerPub: genPeerPub(), peerPub: genPeerPub(),
@@ -264,7 +290,7 @@ func TestAssignPeerPerms(t *testing.T) {
numRestricted: 1, numRestricted: 1,
expectedStatus: peerStatusRestricted, expectedStatus: peerStatusRestricted,
expectedErr: ErrNoMoreRestrictedAccessSlots, expectedErr: nil,
}, },
} }
@@ -394,3 +420,394 @@ func TestAssignPeerPermsBypassRestriction(t *testing.T) {
}) })
} }
} }
// TestAssignPeerPermsBypassExisting asserts that when the peer is a
// pre-existing peer, it won't be restricted.
func TestAssignPeerPermsBypassExisting(t *testing.T) {
t.Parallel()
// genPeerPub is a helper closure that generates a random public key.
genPeerPub := func() *btcec.PublicKey {
peerPriv, err := btcec.NewPrivateKey()
require.NoError(t, err)
return peerPriv.PubKey()
}
// peer1 exists in `peerChanInfo` map.
peer1 := genPeerPub()
peer1Str := string(peer1.SerializeCompressed())
// peer2 exists in `peerScores` map.
peer2 := genPeerPub()
peer2Str := string(peer2.SerializeCompressed())
// peer3 is a new peer.
peer3 := genPeerPub()
// Create params to init the accessman.
initPerms := func() (map[string]channeldb.ChanCount, error) {
return map[string]channeldb.ChanCount{
peer1Str: {},
}, nil
}
disconnect := func(*btcec.PublicKey) (bool, error) {
return false, nil
}
cfg := &accessManConfig{
initAccessPerms: initPerms,
shouldDisconnect: disconnect,
maxRestrictedSlots: 0,
}
a, err := newAccessMan(cfg)
require.NoError(t, err)
// Add peer2 to the `peerScores`.
a.peerScores[peer2Str] = peerSlotStatus{
state: peerStatusTemporary,
}
// Assigning to peer1 should not return an error.
status, err := a.assignPeerPerms(peer1)
require.NoError(t, err)
require.Equal(t, peerStatusRestricted, status)
// Assigning to peer2 should not return an error.
status, err = a.assignPeerPerms(peer2)
require.NoError(t, err)
require.Equal(t, peerStatusTemporary, status)
// Assigning to peer3 should return an error.
status, err = a.assignPeerPerms(peer3)
require.ErrorIs(t, err, ErrNoMoreRestrictedAccessSlots)
require.Equal(t, peerStatusRestricted, status)
}
// TestHasPeer asserts `hasPeer` returns the correct results.
func TestHasPeer(t *testing.T) {
t.Parallel()
ctx := context.Background()
// Create a testing accessMan.
a := &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// peer1 exists with an open channel.
peer1 := "peer1"
a.peerChanInfo[peer1] = channeldb.ChanCount{
HasOpenOrClosedChan: true,
}
peer1Access := peerStatusProtected
// peer2 exists with a pending channel.
peer2 := "peer2"
a.peerChanInfo[peer2] = channeldb.ChanCount{
PendingOpenCount: 1,
}
peer2Access := peerStatusTemporary
// peer3 exists without any channels.
peer3 := "peer3"
a.peerChanInfo[peer3] = channeldb.ChanCount{}
peer3Access := peerStatusRestricted
// peer4 exists with a score.
peer4 := "peer4"
peer4Access := peerStatusTemporary
a.peerScores[peer4] = peerSlotStatus{state: peer4Access}
// peer5 doesn't exist.
peer5 := "peer5"
// We now assert `hasPeer` returns the correct results.
//
// peer1 should be found with peerStatusProtected.
access, found := a.hasPeer(ctx, peer1)
require.True(t, found)
require.Equal(t, peer1Access, access)
// peer2 should be found with peerStatusTemporary.
access, found = a.hasPeer(ctx, peer2)
require.True(t, found)
require.Equal(t, peer2Access, access)
// peer3 should be found with peerStatusRestricted.
access, found = a.hasPeer(ctx, peer3)
require.True(t, found)
require.Equal(t, peer3Access, access)
// peer4 should be found with peerStatusTemporary.
access, found = a.hasPeer(ctx, peer4)
require.True(t, found)
require.Equal(t, peer4Access, access)
// peer5 should NOT be found.
access, found = a.hasPeer(ctx, peer5)
require.False(t, found)
require.Equal(t, peerStatusRestricted, access)
}
// TestAddPeerAccessInbound asserts the num of slots is correctly incremented
// only for a new inbound peer with restricted access.
func TestAddPeerAccessInbound(t *testing.T) {
t.Parallel()
// Create a testing accessMan.
a := &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// Create a testing key.
priv, err := btcec.NewPrivateKey()
require.NoError(t, err)
pub := priv.PubKey()
pubStr := string(pub.SerializeCompressed())
// Add this peer as an inbound peer with peerStatusRestricted.
a.addPeerAccess(pub, peerStatusRestricted, true)
// Assert the accessMan's internal state.
//
// We expect to see one peer found in the score map, and one slot is
// taken, and this peer is not found in the counts map.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(1), a.numRestricted)
require.NotContains(t, a.peerChanInfo, pubStr)
// The peer should be found in the score map.
score, ok := a.peerScores[pubStr]
require.True(t, ok)
expecedScore := peerSlotStatus{state: peerStatusRestricted}
require.Equal(t, expecedScore, score)
// Add this peer again, we expect the available slots to stay unchanged.
a.addPeerAccess(pub, peerStatusRestricted, true)
// Assert the internal state is not changed.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(1), a.numRestricted)
require.NotContains(t, a.peerChanInfo, pubStr)
// Reset the accessMan.
a = &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// Add this peer as an inbound peer with peerStatusTemporary.
a.addPeerAccess(pub, peerStatusTemporary, true)
// Assert the accessMan's internal state.
//
// We expect to see one peer found in the score map, and no slot is
// taken since this peer is not restricted.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(0), a.numRestricted)
// NOTE: in reality this is not possible as the peer must have been put
// into the map `peerChanInfo` before its perm can be upgraded.
require.NotContains(t, a.peerChanInfo, pubStr)
// The peer should be found in the score map.
score, ok = a.peerScores[pubStr]
require.True(t, ok)
expecedScore = peerSlotStatus{state: peerStatusTemporary}
require.Equal(t, expecedScore, score)
}
// TestAddPeerAccessOutbound asserts that outbound peer is not restricted and
// its perm is upgraded when it has peerStatusRestricted.
func TestAddPeerAccessOutbound(t *testing.T) {
t.Parallel()
// Create a testing accessMan.
a := &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// Create a testing key.
priv, err := btcec.NewPrivateKey()
require.NoError(t, err)
pub := priv.PubKey()
pubStr := string(pub.SerializeCompressed())
// Add this peer as an outbound peer with peerStatusRestricted.
a.addPeerAccess(pub, peerStatusRestricted, false)
// Assert the accessMan's internal state.
//
// We expect to see one peer found in the score map, and no slot is
// taken, and this peer is found in the counts map.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(0), a.numRestricted)
require.Contains(t, a.peerChanInfo, pubStr)
// The peer should be found in the score map.
score, ok := a.peerScores[pubStr]
require.True(t, ok)
// Its perm should be upgraded to temporary.
expecedScore := peerSlotStatus{state: peerStatusTemporary}
require.Equal(t, expecedScore, score)
// The peer should be found in the peer counts map.
count, ok := a.peerChanInfo[pubStr]
require.True(t, ok)
// The peer's count should be initialized correctly.
require.Zero(t, count.PendingOpenCount)
require.False(t, count.HasOpenOrClosedChan)
// Add this peer again, we expect the available slots to stay unchanged.
a.addPeerAccess(pub, peerStatusRestricted, true)
// Assert the internal state is not changed.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(0), a.numRestricted)
require.Contains(t, a.peerChanInfo, pubStr)
// Reset the accessMan.
a = &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// Add this peer as an inbound peer with peerStatusTemporary.
a.addPeerAccess(pub, peerStatusTemporary, true)
// Assert the accessMan's internal state.
//
// We expect to see one peer found in the score map, and no slot is
// taken since this peer is not restricted.
require.Len(t, a.peerScores, 1)
require.Equal(t, int64(0), a.numRestricted)
// NOTE: in reality this is not possible as the peer must have been put
// into the map `peerChanInfo` before its perm can be upgraded.
require.NotContains(t, a.peerChanInfo, pubStr)
// The peer should be found in the score map.
score, ok = a.peerScores[pubStr]
require.True(t, ok)
expecedScore = peerSlotStatus{state: peerStatusTemporary}
require.Equal(t, expecedScore, score)
}
// TestRemovePeerAccess asserts `removePeerAccess` correctly update the
// accessman's internal state based on the peer's status.
func TestRemovePeerAccess(t *testing.T) {
t.Parallel()
// Create a testing accessMan.
a := &accessMan{
peerChanInfo: make(map[string]channeldb.ChanCount),
peerScores: make(map[string]peerSlotStatus),
}
// numRestrictedExpected specifies the final value to expect once the
// test finishes.
var numRestrictedExpected int
// peer1 exists with an open channel, which should not be removed. Since
// it has protected status, the numRestricted should stay unchanged.
peer1 := "peer1"
a.peerChanInfo[peer1] = channeldb.ChanCount{
HasOpenOrClosedChan: true,
}
peer1Access := peerStatusProtected
a.peerScores[peer1] = peerSlotStatus{state: peer1Access}
// peer2 exists with a pending channel, which should not be removed.
// Since it has temporary status, the numRestricted should stay
// unchanged.
peer2 := "peer2"
a.peerChanInfo[peer2] = channeldb.ChanCount{
PendingOpenCount: 1,
}
peer2Access := peerStatusTemporary
a.peerScores[peer2] = peerSlotStatus{state: peer2Access}
// peer3 exists without any channels, which will be removed. Since it
// has restricted status, the numRestricted should be decremented.
peer3 := "peer3"
a.peerChanInfo[peer3] = channeldb.ChanCount{}
peer3Access := peerStatusRestricted
a.peerScores[peer3] = peerSlotStatus{state: peer3Access}
numRestrictedExpected--
// peer4 exists with a score and a temporary status, which will be
// removed.
peer4 := "peer4"
peer4Access := peerStatusTemporary
a.peerScores[peer4] = peerSlotStatus{state: peer4Access}
// peer5 doesn't exist, removing it will be a NOOP.
peer5 := "peer5"
// We now assert `removePeerAccess` behaves as expected.
//
// Remove peer1 should change nothing.
a.removePeerAccess(peer1)
// peer1 should be removed from peerScores but not peerChanInfo.
_, found := a.peerScores[peer1]
require.False(t, found)
_, found = a.peerChanInfo[peer1]
require.True(t, found)
// Remove peer2 should change nothing.
a.removePeerAccess(peer2)
// peer2 should be removed from peerScores but not peerChanInfo.
_, found = a.peerScores[peer2]
require.False(t, found)
_, found = a.peerChanInfo[peer2]
require.True(t, found)
// Remove peer3 should remove it from the maps.
a.removePeerAccess(peer3)
// peer3 should be removed from peerScores and peerChanInfo.
_, found = a.peerScores[peer3]
require.False(t, found)
_, found = a.peerChanInfo[peer3]
require.False(t, found)
// Remove peer4 should remove it from the maps.
a.removePeerAccess(peer4)
// peer4 should be removed from peerScores and NOT be found in
// peerChanInfo.
_, found = a.peerScores[peer4]
require.False(t, found)
_, found = a.peerChanInfo[peer4]
require.False(t, found)
// Remove peer5 should be NOOP.
a.removePeerAccess(peer5)
// peer5 should NOT be found.
_, found = a.peerScores[peer5]
require.False(t, found)
_, found = a.peerChanInfo[peer5]
require.False(t, found)
// Finally, assert the numRestricted is decremented as expected. Given
// we only have peer3 which has restricted access, it should decrement
// once.
//
// NOTE: The value is actually negative here, which is allowed in
// accessman.
require.EqualValues(t, numRestrictedExpected, a.numRestricted)
}
+7 -7
View File
@@ -743,7 +743,7 @@ type ChanCount struct {
func (c *ChannelStateDB) FetchPermAndTempPeers( func (c *ChannelStateDB) FetchPermAndTempPeers(
chainHash []byte) (map[string]ChanCount, error) { chainHash []byte) (map[string]ChanCount, error) {
peerCounts := make(map[string]ChanCount) peerChanInfo := make(map[string]ChanCount)
err := kvdb.View(c.backend, func(tx kvdb.RTx) error { err := kvdb.View(c.backend, func(tx kvdb.RTx) error {
openChanBucket := tx.ReadBucket(openChannelBucket) openChanBucket := tx.ReadBucket(openChannelBucket)
@@ -829,7 +829,7 @@ func (c *ChannelStateDB) FetchPermAndTempPeers(
HasOpenOrClosedChan: isPermPeer, HasOpenOrClosedChan: isPermPeer,
PendingOpenCount: pendingOpenCount, PendingOpenCount: pendingOpenCount,
} }
peerCounts[string(nodePub)] = peerCount peerChanInfo[string(nodePub)] = peerCount
return nil return nil
}) })
@@ -893,15 +893,15 @@ func (c *ChannelStateDB) FetchPermAndTempPeers(
remoteSer := remotePub.SerializeCompressed() remoteSer := remotePub.SerializeCompressed()
remoteKey := string(remoteSer) remoteKey := string(remoteSer)
count, exists := peerCounts[remoteKey] count, exists := peerChanInfo[remoteKey]
if exists { if exists {
count.HasOpenOrClosedChan = true count.HasOpenOrClosedChan = true
peerCounts[remoteKey] = count peerChanInfo[remoteKey] = count
} else { } else {
peerCount := ChanCount{ peerCount := ChanCount{
HasOpenOrClosedChan: true, HasOpenOrClosedChan: true,
} }
peerCounts[remoteKey] = peerCount peerChanInfo[remoteKey] = peerCount
} }
} }
@@ -913,10 +913,10 @@ func (c *ChannelStateDB) FetchPermAndTempPeers(
return nil return nil
}, func() { }, func() {
clear(peerCounts) clear(peerChanInfo)
}) })
return peerCounts, err return peerChanInfo, err
} }
// channelSelector describes a function that takes a chain-hash bucket from // channelSelector describes a function that takes a chain-hash bucket from
+8 -8
View File
@@ -770,16 +770,16 @@ func TestFetchPermTempPeer(t *testing.T) {
) )
// Fetch the ChanCount for our peers. // Fetch the ChanCount for our peers.
peerCounts, err := cdb.FetchPermAndTempPeers(key[:]) peerChanInfo, err := cdb.FetchPermAndTempPeers(key[:])
require.NoError(t, err, "unable to fetch perm and temp peers") require.NoError(t, err, "unable to fetch perm and temp peers")
// There should only be three entries. // There should only be three entries.
require.Len(t, peerCounts, 3) require.Len(t, peerChanInfo, 3)
// The first entry should have OpenClosed set to true and Pending set // The first entry should have OpenClosed set to true and Pending set
// to 0. // to 0.
count1, found := peerCounts[string(pubKey1.SerializeCompressed())] count1, found := peerChanInfo[string(pubKey1.SerializeCompressed())]
require.True(t, found, "unable to find peer 1 in peerCounts") require.True(t, found, "unable to find peer 1 in peerChanInfo")
require.True( require.True(
t, count1.HasOpenOrClosedChan, t, count1.HasOpenOrClosedChan,
"couldn't find peer 1's channels", "couldn't find peer 1's channels",
@@ -789,15 +789,15 @@ func TestFetchPermTempPeer(t *testing.T) {
"peer 1 doesn't have 0 pending-open", "peer 1 doesn't have 0 pending-open",
) )
count2, found := peerCounts[string(pubKey2.SerializeCompressed())] count2, found := peerChanInfo[string(pubKey2.SerializeCompressed())]
require.True(t, found, "unable to find peer 2 in peerCounts") require.True(t, found, "unable to find peer 2 in peerChanInfo")
require.False( require.False(
t, count2.HasOpenOrClosedChan, "found erroneous channels", t, count2.HasOpenOrClosedChan, "found erroneous channels",
) )
require.Equal(t, uint64(1), count2.PendingOpenCount) require.Equal(t, uint64(1), count2.PendingOpenCount)
count3, found := peerCounts[string(pubKey3.SerializeCompressed())] count3, found := peerChanInfo[string(pubKey3.SerializeCompressed())]
require.True(t, found, "unable to find peer 3 in peerCounts") require.True(t, found, "unable to find peer 3 in peerChanInfo")
require.True( require.True(
t, count3.HasOpenOrClosedChan, t, count3.HasOpenOrClosedChan,
"couldn't find peer 3's channels", "couldn't find peer 3's channels",
+6 -5
View File
@@ -238,8 +238,9 @@ const (
// defaultHTTPHeaderTimeout is the default timeout for HTTP requests. // defaultHTTPHeaderTimeout is the default timeout for HTTP requests.
DefaultHTTPHeaderTimeout = 5 * time.Second DefaultHTTPHeaderTimeout = 5 * time.Second
// DefaultNumRestrictedSlots is the default number of restricted slots // DefaultNumRestrictedSlots is the default max number of incoming
// we'll allocate in the server. // connections allowed in the server. Outbound connections are not
// restricted.
DefaultNumRestrictedSlots = 100 DefaultNumRestrictedSlots = 100
// BitcoinChainName is a string that represents the Bitcoin blockchain. // BitcoinChainName is a string that represents the Bitcoin blockchain.
@@ -529,9 +530,9 @@ type Config struct {
// before timing out reading the headers of an HTTP request. // before timing out reading the headers of an HTTP request.
HTTPHeaderTimeout time.Duration `long:"http-header-timeout" description:"The maximum duration that the server will wait before timing out reading the headers of an HTTP request."` HTTPHeaderTimeout time.Duration `long:"http-header-timeout" description:"The maximum duration that the server will wait before timing out reading the headers of an HTTP request."`
// NumRestrictedSlots is the number of restricted slots we'll allocate // NumRestrictedSlots is the max number of incoming connections allowed
// in the server. // in the server. Outbound connections are not restricted.
NumRestrictedSlots uint64 `long:"num-restricted-slots" description:"The number of restricted slots we'll allocate in the server."` NumRestrictedSlots uint64 `long:"num-restricted-slots" description:"The max number of incoming connections allowed in the server. Outbound connections are not restricted."`
// NoDisconnectOnPongFailure controls if we'll disconnect if a peer // NoDisconnectOnPongFailure controls if we'll disconnect if a peer
// doesn't respond to a pong in time. // doesn't respond to a pong in time.
+4 -1
View File
@@ -41,6 +41,10 @@
## Functional Updates ## Functional Updates
- [Improved](https://github.com/lightningnetwork/lnd/pull/9880) the connection
restriction logic enforced by `accessman`. In addition, the restriction placed
on outbound connections is now lifted.
## RPC Updates ## RPC Updates
## lncli Updates ## lncli Updates
@@ -71,5 +75,4 @@ much more slowly.
## Tooling and Documentation ## Tooling and Documentation
# Contributors (Alphabetical Order) # Contributors (Alphabetical Order)
* Yong Yu * Yong Yu
+12 -31
View File
@@ -511,7 +511,7 @@ type Config struct {
// NotifyOpenChannelEvent informs the ChannelNotifier when channels // NotifyOpenChannelEvent informs the ChannelNotifier when channels
// transition from pending open to open. // transition from pending open to open.
NotifyOpenChannelEvent func(wire.OutPoint, *btcec.PublicKey) error NotifyOpenChannelEvent func(wire.OutPoint, *btcec.PublicKey)
// OpenChannelPredicate is a predicate on the lnwire.OpenChannel message // OpenChannelPredicate is a predicate on the lnwire.OpenChannel message
// and on the requesting node's public key that returns a bool which // and on the requesting node's public key that returns a bool which
@@ -521,13 +521,13 @@ type Config struct {
// NotifyPendingOpenChannelEvent informs the ChannelNotifier when // NotifyPendingOpenChannelEvent informs the ChannelNotifier when
// channels enter a pending state. // channels enter a pending state.
NotifyPendingOpenChannelEvent func(wire.OutPoint, NotifyPendingOpenChannelEvent func(wire.OutPoint,
*channeldb.OpenChannel, *btcec.PublicKey) error *channeldb.OpenChannel, *btcec.PublicKey)
// NotifyFundingTimeout informs the ChannelNotifier when a pending-open // NotifyFundingTimeout informs the ChannelNotifier when a pending-open
// channel times out because the funding transaction hasn't confirmed. // channel times out because the funding transaction hasn't confirmed.
// This is only called for the fundee and only if the channel is // This is only called for the fundee and only if the channel is
// zero-conf. // zero-conf.
NotifyFundingTimeout func(wire.OutPoint, *btcec.PublicKey) error NotifyFundingTimeout func(wire.OutPoint, *btcec.PublicKey)
// EnableUpfrontShutdown specifies whether the upfront shutdown script // EnableUpfrontShutdown specifies whether the upfront shutdown script
// is enabled. // is enabled.
@@ -1319,13 +1319,9 @@ func (f *Manager) advancePendingChannelState(channel *channeldb.OpenChannel,
// Inform the ChannelNotifier that the channel has transitioned // Inform the ChannelNotifier that the channel has transitioned
// from pending open to open. // from pending open to open.
if err := f.cfg.NotifyOpenChannelEvent( f.cfg.NotifyOpenChannelEvent(
channel.FundingOutpoint, channel.IdentityPub, channel.FundingOutpoint, channel.IdentityPub,
); err != nil { )
log.Errorf("Unable to notify open channel event for "+
"ChannelPoint(%v): %v",
channel.FundingOutpoint, err)
}
// Find and close the discoverySignal for this channel such // Find and close the discoverySignal for this channel such
// that ChannelReady messages will be processed. // that ChannelReady messages will be processed.
@@ -2666,12 +2662,9 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
// Inform the ChannelNotifier that the channel has entered // Inform the ChannelNotifier that the channel has entered
// pending open state. // pending open state.
if err := f.cfg.NotifyPendingOpenChannelEvent( f.cfg.NotifyPendingOpenChannelEvent(
fundingOut, completeChan, completeChan.IdentityPub, fundingOut, completeChan, completeChan.IdentityPub,
); err != nil { )
log.Errorf("Unable to send pending-open channel event for "+
"ChannelPoint(%v) %v", fundingOut, err)
}
// At this point we have sent our last funding message to the // At this point we have sent our last funding message to the
// initiating peer before the funding transaction will be broadcast. // initiating peer before the funding transaction will be broadcast.
@@ -2891,13 +2884,9 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
case resCtx.updates <- upd: case resCtx.updates <- upd:
// Inform the ChannelNotifier that the channel has entered // Inform the ChannelNotifier that the channel has entered
// pending open state. // pending open state.
if err := f.cfg.NotifyPendingOpenChannelEvent( f.cfg.NotifyPendingOpenChannelEvent(
*fundingPoint, completeChan, completeChan.IdentityPub, *fundingPoint, completeChan, completeChan.IdentityPub,
); err != nil { )
log.Errorf("Unable to send pending-open channel "+
"event for ChannelPoint(%v) %v", fundingPoint,
err)
}
case <-f.quit: case <-f.quit:
return return
@@ -2955,11 +2944,7 @@ func (f *Manager) fundingTimeout(c *channeldb.OpenChannel,
} }
// Notify other subsystems about the funding timeout. // Notify other subsystems about the funding timeout.
err := f.cfg.NotifyFundingTimeout(c.FundingOutpoint, c.IdentityPub) f.cfg.NotifyFundingTimeout(c.FundingOutpoint, c.IdentityPub)
if err != nil {
log.Errorf("failed to notify of funding timeout for "+
"ChanPoint(%v): %v", c.FundingOutpoint, err)
}
timeoutErr := fmt.Errorf("timeout waiting for funding tx (%v) to "+ timeoutErr := fmt.Errorf("timeout waiting for funding tx (%v) to "+
"confirm", c.FundingOutpoint) "confirm", c.FundingOutpoint)
@@ -3341,13 +3326,9 @@ func (f *Manager) handleFundingConfirmation(
// Inform the ChannelNotifier that the channel has transitioned from // Inform the ChannelNotifier that the channel has transitioned from
// pending open to open. // pending open to open.
if err := f.cfg.NotifyOpenChannelEvent( f.cfg.NotifyOpenChannelEvent(
completeChan.FundingOutpoint, completeChan.IdentityPub, completeChan.FundingOutpoint, completeChan.IdentityPub,
); err != nil { )
log.Errorf("Unable to notify open channel event for "+
"ChannelPoint(%v): %v", completeChan.FundingOutpoint,
err)
}
// Close the discoverySignal channel, indicating to a separate // Close the discoverySignal channel, indicating to a separate
// goroutine that the channel now is marked as open in the database // goroutine that the channel now is marked as open in the database
+3 -9
View File
@@ -232,29 +232,23 @@ type mockChanEvent struct {
} }
func (m *mockChanEvent) NotifyOpenChannelEvent(outpoint wire.OutPoint, func (m *mockChanEvent) NotifyOpenChannelEvent(outpoint wire.OutPoint,
remotePub *btcec.PublicKey) error { remotePub *btcec.PublicKey) {
m.openEvent <- outpoint m.openEvent <- outpoint
return nil
} }
func (m *mockChanEvent) NotifyPendingOpenChannelEvent(outpoint wire.OutPoint, func (m *mockChanEvent) NotifyPendingOpenChannelEvent(outpoint wire.OutPoint,
pendingChannel *channeldb.OpenChannel, pendingChannel *channeldb.OpenChannel,
remotePub *btcec.PublicKey) error { remotePub *btcec.PublicKey) {
m.pendingOpenEvent <- channelnotifier.PendingOpenChannelEvent{ m.pendingOpenEvent <- channelnotifier.PendingOpenChannelEvent{
ChannelPoint: &outpoint, ChannelPoint: &outpoint,
PendingChannel: pendingChannel, PendingChannel: pendingChannel,
} }
return nil
} }
func (m *mockChanEvent) NotifyFundingTimeout(outpoint wire.OutPoint, func (m *mockChanEvent) NotifyFundingTimeout(outpoint wire.OutPoint,
remotePub *btcec.PublicKey) error { remotePub *btcec.PublicKey) {
return nil
} }
// mockZeroConfAcceptor always accepts the channel open request for zero-conf // mockZeroConfAcceptor always accepts the channel open request for zero-conf
+3 -4
View File
@@ -691,10 +691,6 @@ var allTestCases = []*lntest.TestCase{
Name: "funding manager funding timeout", Name: "funding manager funding timeout",
TestFunc: testFundingManagerFundingTimeout, TestFunc: testFundingManagerFundingTimeout,
}, },
{
Name: "access perm",
TestFunc: testAccessPerm,
},
{ {
Name: "rbf coop close", Name: "rbf coop close",
TestFunc: testCoopCloseRbf, TestFunc: testCoopCloseRbf,
@@ -782,6 +778,9 @@ func init() {
"coop close with external delivery", allTestCases, "coop close with external delivery", allTestCases,
coopCloseWithExternalTestCases, coopCloseWithExternalTestCases,
) )
allTestCases = appendPrefixed(
"peer conn", allTestCases, peerConnTestCases,
)
// Prepare the test cases for windows to exclude some of the flaky // Prepare the test cases for windows to exclude some of the flaky
// ones. // ones.
+325 -77
View File
@@ -1,93 +1,341 @@
package itest package itest
import ( import (
"strconv"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/lntest"
"github.com/stretchr/testify/require"
) )
// testAccessPerm tests that the number of restricted slots is upheld when var peerConnTestCases = []*lntest.TestCase{
// connecting to the server from a restrictedd peer. {
func testAccessPerm(ht *lntest.HarnessTest) { Name: "restricted on inbound",
args := []string{ TestFunc: testPeerConnRestrictedOnInbound,
"--minbackoff=5m", },
"--maxbackoff=5m", {
"--num-restricted-slots=5", Name: "already connected",
} TestFunc: testPeerConnAlreadyConnected,
},
{
Name: "unlimited outbound",
TestFunc: testPeerConnUnlimitedOutbound,
},
{
Name: "upgrade access perm",
TestFunc: testPeerConnUpgradeAccessPerm,
},
{
Name: "node restart",
TestFunc: testPeerConnNodeRestart,
},
{
Name: "peer reconnect",
TestFunc: testPeerConnPeerReconnect,
},
}
alice := ht.NewNodeWithCoins("Alice", args) // testPeerConnRestrictedOnInbound checks that when the `num-restricted-slots`
bob := ht.NewNodeWithCoins("Bob", args) // is reached, no more inbound connection is allowed. In addition, when a slot
ht.ConnectNodes(alice, bob) // becomes available, new inbound connection should be allowed.
func testPeerConnRestrictedOnInbound(ht *lntest.HarnessTest) {
args := []string{"--num-restricted-slots=1"}
// Open a confirmed channel to Bob. Bob will have protected access. // Create a new node with only one slot available.
chanPoint1 := ht.OpenChannel( alice := ht.NewNode("Alice", args)
alice, bob, lntest.OpenChannelParams{
Amt: chanAmt,
},
)
defer ht.CloseChannel(alice, chanPoint1)
// Open and close channel to Carol. Carol will have protected access. // Create two nodes Bob and Carol. Bob will connect to Alice first,
carol := ht.NewNodeWithCoins("Carol", args) // taking up Alice's only slot, and we expect the connection from Carol
ht.ConnectNodes(alice, carol) // to Alice to be failed.
bob := ht.NewNode("Bob", nil)
carol := ht.NewNode("Carol", nil)
chanPoint2 := ht.OpenChannel( // Bob now connects to Alice - from Alice's PoV, this is her inbound
alice, carol, lntest.OpenChannelParams{ // connection from Bob. We expect Bob to connect to Alice successfully.
Amt: chanAmt, _, err := ht.ConnectNodesNoAssert(bob, alice)
}, require.NoError(ht, err)
)
ht.CloseChannel(alice, chanPoint2)
// Make a pending channel with Dave.
dave := ht.NewNodeWithCoins("Dave", args)
ht.ConnectNodes(alice, dave)
ht.OpenChannelAssertStream(
dave, alice, lntest.OpenChannelParams{
Amt: chanAmt,
},
)
// Disconnect Bob, Carol, and Dave.
ht.DisconnectNodes(alice, bob)
ht.AssertNotConnected(alice, bob)
ht.DisconnectNodes(alice, carol)
ht.AssertNotConnected(alice, carol)
ht.DisconnectNodes(alice, dave)
ht.AssertNotConnected(alice, dave)
// Connect 5 times to Alice. All of these connections should be
// successful.
for i := 0; i < 5; i++ {
peer := ht.NewNode("Peer"+strconv.Itoa(i), args)
ht.ConnectNodes(peer, alice)
ht.AssertConnected(peer, alice)
}
// Connect an additional time to Alice. This should fail.
failedPeer := ht.NewNode("FailedPeer", args)
req := &lnrpc.ConnectPeerRequest{
Addr: &lnrpc.LightningAddress{
Pubkey: alice.RPC.GetInfo().IdentityPubkey,
Host: alice.Cfg.P2PAddr(),
},
}
_ = failedPeer.RPC.ConnectPeer(req)
ht.AssertNotConnected(failedPeer, alice)
// Connect nodes and assert access status.
ht.ConnectNodes(alice, bob)
ht.AssertConnected(alice, bob) ht.AssertConnected(alice, bob)
ht.ConnectNodes(alice, carol) // Carol now connects to Alice - from Alice's PoV, this is her inbound
// connection from Carol. Carol's connection should be failed.
_, err = ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Bob now disconnects, which will free up Alice's slot.
ht.DisconnectNodes(bob, alice)
// Carol connects again - since Alice's slot is freed, this connection
// should succeed.
_, err = ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertConnected(alice, carol)
ht.AssertNotConnected(alice, bob)
}
// testPeerConnAlreadyConnected checks that when there's already a connection
// alive, another attempt to connect doesn't take up the slot or kill the
// existing connection, in specific,
// - When Alice has an inbound connection from Bob, another inbound connection
// from Bob will be noop.
// - When Bob has an outbound connection to Alice, another outbound connection
// to Alice will be noop.
//
// In this test we will create nodes using the dev flag `unsafeconnect` to mimic
// the behaviour of persistent/permanent peer connections, where it's likely
// inbound and outbound connections are happening at the same time.
func testPeerConnAlreadyConnected(ht *lntest.HarnessTest) {
args := []string{
"--num-restricted-slots=1",
}
// Create a new node with two slots available.
alice := ht.NewNode("Alice", args)
bob := ht.NewNode("Bob", []string{"--dev.unsafeconnect"})
carol := ht.NewNode("Carol", nil)
// Bob now connects to Alice - from Alice's PoV, this is her inbound
// connection from Bob. We expect Bob to connect to Alice successfully.
_, err := ht.ConnectNodesNoAssert(bob, alice)
require.NoError(ht, err)
ht.AssertConnected(alice, bob)
// Assert Alice's slot has been filled up by connecting Carol to Alice.
_, err = ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Bob connects to Alice again - from Alice's PoV, she already has an
// inbound connection from Bob so this connection attempt should be
// noop. We expect Alice and Bob to stay connected.
//
// TODO(yy): There's no way to assert the connection stays the same atm,
// need to update the RPC to return the socket port. As for now we need
// to visit the logs to check the connection is the same or not.
_, err = ht.ConnectNodesNoAssert(bob, alice)
require.NoError(ht, err)
ht.AssertConnected(alice, bob)
}
// testPeerConnUnlimitedOutbound checks that for outbound connections, they are
// not restricted by the `num-restricted-slots` config.
func testPeerConnUnlimitedOutbound(ht *lntest.HarnessTest) {
args := []string{"--num-restricted-slots=1"}
// Create a new node with one slot available.
alice := ht.NewNode("Alice", args)
// Create three nodes. Alice will have an inbound connection with Bob
// and outbound connections with Carol and Dave.
bob := ht.NewNode("Bob", args)
carol := ht.NewNode("Carol", args)
dave := ht.NewNode("Dave", args)
// Bob now connects to Alice - from Alice's PoV, this is her inbound
// connection from Bob. We expect Bob to connect to Alice successfully.
_, err := ht.ConnectNodesNoAssert(bob, alice)
require.NoError(ht, err)
ht.AssertConnected(alice, bob)
// Assert Alice's slot has been filled up by connecting Carol to Alice.
_, err = ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Now let Alice make an outbound connection to Carol and assert it's
// succeeded.
_, err = ht.ConnectNodesNoAssert(alice, carol)
require.NoError(ht, err)
ht.AssertConnected(alice, carol) ht.AssertConnected(alice, carol)
ht.ConnectNodes(alice, dave) // Alice can also make an outbound connection to Dave and assert it's
// succeeded since outbound connection is not restricted.
_, err = ht.ConnectNodesNoAssert(alice, dave)
require.NoError(ht, err)
ht.AssertConnected(alice, dave) ht.AssertConnected(alice, dave)
}
ht.MineBlocksAndAssertNumTxes(1, 1)
// testPeerConnUpgradeAccessPerm checks that when a peer has, or used to have a
// channel with Alice, it won't use her restricted slots.
func testPeerConnUpgradeAccessPerm(ht *lntest.HarnessTest) {
args := []string{"--num-restricted-slots=1"}
// Create a new node with one slot available.
alice := ht.NewNodeWithCoins("Alice", args)
bob := ht.NewNode("Bob", nil)
carol := ht.NewNodeWithCoins("Carol", nil)
dave := ht.NewNode("Dave", nil)
eve := ht.NewNode("Eve", nil)
// Connect Bob to Alice, which will use Alice's available slot.
ht.ConnectNodes(bob, alice)
// Assert Alice's slot has been filled up by connecting Carol to Alice.
_, err := ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Open a channel from Alice to Bob and let it stay pending.
p := lntest.OpenChannelParams{
Amt: chanAmt,
}
pendingUpdate := ht.OpenChannelAssertPending(alice, bob, p)
cpAB := lntest.ChanPointFromPendingUpdate(pendingUpdate)
// Connect Carol to Alice - since Bob now has a pending channel with
// Alice, there's a slot available for Carol to connect.
ht.ConnectNodes(carol, alice)
// Open a channel from Carol to Alice and let it stay pending.
pendingUpdate = ht.OpenChannelAssertPending(carol, alice, p)
cpCA := lntest.ChanPointFromPendingUpdate(pendingUpdate)
// Mine the funding txns.
ht.MineBlocksAndAssertNumTxes(1, 2)
// Dave should be able to connect to Alice.
ht.ConnectNodes(dave, alice)
// Open a channel from Alice to Dave, which will free up Alice's slot.
cpAD := ht.OpenChannel(alice, dave, p)
// Close the channels.
ht.CloseChannel(bob, cpAB)
ht.CloseChannel(carol, cpCA)
ht.CloseChannel(dave, cpAD)
// Alice should have one slot available, connect Eve to Alice now.
ht.ConnectNodes(eve, alice)
}
// testPeerConnNodeRestart checks that when a peer has or used to have a channel
// with Alice, when Alice restarts, she should still have the available slot for
// more inbound connections.
func testPeerConnNodeRestart(ht *lntest.HarnessTest) {
args := []string{"--num-restricted-slots=1"}
// Create a new node with one slot available.
alice := ht.NewNodeWithCoins("Alice", args)
bob := ht.NewNode("Bob", []string{"--dev.unsafeconnect"})
carol := ht.NewNode("Carol", nil)
// Connect Bob to Alice, which will use Alice's available slot.
ht.ConnectNodes(bob, alice)
// Assert Alice's slot has been filled up by connecting Carol to Alice.
_, err := ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Restart Alice, which will reset her current slots, allowing Bob to
// connect to her again.
ht.RestartNode(alice)
ht.ConnectNodes(bob, alice)
// Open a channel from Alice to Bob and let it stay pending.
p := lntest.OpenChannelParams{
Amt: chanAmt,
}
pendingUpdate := ht.OpenChannelAssertPending(alice, bob, p)
cp := lntest.ChanPointFromPendingUpdate(pendingUpdate)
// Restart Alice and let Bob connect to her - since Bob has a pending
// channel, it shouldn't take any slot.
ht.RestartNode(alice)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
// Mine the funding tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Restart Alice and let Bob connect to her - since Bob has an open
// channel, it shouldn't take any slot.
ht.RestartNode(alice)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
// Close the channel.
ht.CloseChannel(alice, cp)
// Restart Alice and let Bob connect to her - since Bob has a closed
// channel, it shouldn't take any slot.
ht.RestartNode(alice)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
}
// testPeerConnPeerReconnect checks that when a peer has or used to have a
// channel with Alice, it will not account for the restricted slot during
// reconnection.
func testPeerConnPeerReconnect(ht *lntest.HarnessTest) {
args := []string{"--num-restricted-slots=1"}
// Create a new node with one slot available.
alice := ht.NewNodeWithCoins("Alice", args)
bob := ht.NewNode("Bob", []string{"--dev.unsafeconnect"})
carol := ht.NewNode("Carol", nil)
// Connect Bob to Alice, which will use Alice's available slot.
ht.ConnectNodes(bob, alice)
// Let Bob connect to Alice again, which put Bob in Alice's
// `scheduledPeerConnection` map.
ht.ConnectNodes(bob, alice)
// Assert Alice's slot has been filled up by connecting Carol to Alice.
_, err := ht.ConnectNodesNoAssert(carol, alice)
require.NoError(ht, err)
ht.AssertNotConnected(alice, carol)
// Open a channel from Alice to Bob and let it stay pending.
p := lntest.OpenChannelParams{
Amt: chanAmt,
}
pendingUpdate := ht.OpenChannelAssertPending(alice, bob, p)
cp := lntest.ChanPointFromPendingUpdate(pendingUpdate)
// Bob now perform a reconnection - since Bob has a pending channel, it
// shouldn't take any slot.
ht.DisconnectNodes(bob, alice)
ht.AssertNotConnected(alice, bob)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
// Once the above connection succeeded we let Carol disconnect to free
// the slot.
ht.DisconnectNodes(carol, alice)
// Mine the funding tx.
ht.MineBlocksAndAssertNumTxes(1, 1)
// Bob now perform a reconnection - since Bob has a pending channel, it
// shouldn't take any slot.
ht.DisconnectNodes(bob, alice)
ht.AssertNotConnected(alice, bob)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
// Once the above connection succeeded we let Carol disconnect to free
// the slot.
ht.DisconnectNodes(carol, alice)
// Close the channel.
ht.CloseChannel(alice, cp)
// Bob now perform a reconnection - since Bob has a pending channel, it
// shouldn't take any slot.
ht.DisconnectNodes(bob, alice)
ht.AssertNotConnected(alice, bob)
ht.ConnectNodes(bob, alice)
// Connect Carol to Alice since Alice has a free slot.
ht.ConnectNodes(carol, alice)
} }
+6
View File
@@ -52,3 +52,9 @@ func (d *DevConfig) GetZombieSweeperInterval() time.Duration {
func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 { func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
return DefaultMaxWaitNumBlocksFundingConf return DefaultMaxWaitNumBlocksFundingConf
} }
// GetUnsafeConnect returns the config value `UnsafeConnect`, which is always
// false for production build.
func (d *DevConfig) GetUnsafeConnect() bool {
return false
}
+7 -1
View File
@@ -26,6 +26,7 @@ type DevConfig struct {
ZombieSweeperInterval time.Duration `long:"zombiesweeperinterval" description:"The time interval at which channel opening flows are evaluated for zombie status."` ZombieSweeperInterval time.Duration `long:"zombiesweeperinterval" description:"The time interval at which channel opening flows are evaluated for zombie status."`
UnsafeDisconnect bool `long:"unsafedisconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels."` UnsafeDisconnect bool `long:"unsafedisconnect" description:"Allows the rpcserver to intentionally disconnect from peers with open channels."`
MaxWaitNumBlocksFundingConf uint32 `long:"maxwaitnumblocksfundingconf" description:"Maximum blocks to wait for funding confirmation before discarding non-initiated channels."` MaxWaitNumBlocksFundingConf uint32 `long:"maxwaitnumblocksfundingconf" description:"Maximum blocks to wait for funding confirmation before discarding non-initiated channels."`
UnsafeConnect bool `long:"unsafeconnect" description:"Allow the rpcserver to connect to a peer even if there's already a connection."`
} }
// ChannelReadyWait returns the config value `ProcessChannelReadyWait`. // ChannelReadyWait returns the config value `ProcessChannelReadyWait`.
@@ -51,7 +52,7 @@ func (d *DevConfig) GetZombieSweeperInterval() time.Duration {
return d.ZombieSweeperInterval return d.ZombieSweeperInterval
} }
// ChannelReadyWait returns the config value `UnsafeDisconnect`. // GetUnsafeDisconnect returns the config value `UnsafeDisconnect`.
func (d *DevConfig) GetUnsafeDisconnect() bool { func (d *DevConfig) GetUnsafeDisconnect() bool {
return d.UnsafeDisconnect return d.UnsafeDisconnect
} }
@@ -65,3 +66,8 @@ func (d *DevConfig) GetMaxWaitNumBlocksFundingConf() uint32 {
return d.MaxWaitNumBlocksFundingConf return d.MaxWaitNumBlocksFundingConf
} }
// GetUnsafeConnect returns the config value `UnsafeConnect`.
func (d *DevConfig) GetUnsafeConnect() bool {
return d.UnsafeConnect
}
+24 -1
View File
@@ -119,7 +119,7 @@ func (h *HarnessTest) ConnectNodes(a, b *node.HarnessNode) {
}, },
} }
a.RPC.ConnectPeer(req) a.RPC.ConnectPeer(req)
h.AssertPeerConnected(a, b) h.AssertConnected(a, b)
} }
// ConnectNodesPerm creates a persistent connection between the two nodes and // ConnectNodesPerm creates a persistent connection between the two nodes and
@@ -240,6 +240,24 @@ func (h *HarnessTest) EnsureConnected(a, b *node.HarnessNode) {
h.AssertPeerConnected(b, a) h.AssertPeerConnected(b, a)
} }
// ConnectNodesNoAssert creates a connection from node A to node B.
func (h *HarnessTest) ConnectNodesNoAssert(a, b *node.HarnessNode) (
*lnrpc.ConnectPeerResponse, error) {
bobInfo := b.RPC.GetInfo()
req := &lnrpc.ConnectPeerRequest{
Addr: &lnrpc.LightningAddress{
Pubkey: bobInfo.IdentityPubkey,
Host: b.Cfg.P2PAddr(),
},
}
ctxt, cancel := context.WithTimeout(h.runCtx, DefaultTimeout)
defer cancel()
return a.RPC.LN.ConnectPeer(ctxt, req)
}
// AssertNumEdges checks that an expected number of edges can be found in the // AssertNumEdges checks that an expected number of edges can be found in the
// node specified. // node specified.
func (h *HarnessTest) AssertNumEdges(hn *node.HarnessNode, func (h *HarnessTest) AssertNumEdges(hn *node.HarnessNode,
@@ -1669,6 +1687,11 @@ func (h *HarnessTest) AssertPeerNotConnected(a, b *node.HarnessNode) {
// AssertNotConnected asserts that two peers are not connected. // AssertNotConnected asserts that two peers are not connected.
func (h *HarnessTest) AssertNotConnected(a, b *node.HarnessNode) { func (h *HarnessTest) AssertNotConnected(a, b *node.HarnessNode) {
// Sleep one second before the assertion to make sure that when there's
// a RPC call to connect, that RPC call is finished before the
// assertion.
time.Sleep(1 * time.Second)
h.AssertPeerNotConnected(a, b) h.AssertPeerNotConnected(a, b)
h.AssertPeerNotConnected(b, a) h.AssertPeerNotConnected(b, a)
} }
+2 -1
View File
@@ -566,7 +566,8 @@
; the headers of an HTTP request. ; the headers of an HTTP request.
; http-header-timeout=5s ; http-header-timeout=5s
; The number of restricted slots the server will allocate for peers. ; The max number of incoming connections allowed in the server. Outbound
; connections are not restricted.
; num-restricted-slots=100 ; num-restricted-slots=100
; If true, a peer will *not* be disconnected if a pong is not received in time ; If true, a peer will *not* be disconnected if a pong is not received in time
+71 -56
View File
@@ -193,6 +193,23 @@ const (
peerStatusProtected peerStatusProtected
) )
// String returns a human-readable representation of the status code.
func (p peerAccessStatus) String() string {
switch p {
case peerStatusRestricted:
return "restricted"
case peerStatusTemporary:
return "temporary"
case peerStatusProtected:
return "protected"
default:
return "unknown"
}
}
// peerSlotStatus determines whether a peer gets access to one of our free // peerSlotStatus determines whether a peer gets access to one of our free
// slots or gets to bypass this safety mechanism. // slots or gets to bypass this safety mechanism.
type peerSlotStatus struct { type peerSlotStatus struct {
@@ -1891,7 +1908,9 @@ func newServer(ctx context.Context, cfg *Config, listenAddrs []net.Addr,
// connection requests when we call NewListener. // connection requests when we call NewListener.
listeners[i], err = brontide.NewListener( listeners[i], err = brontide.NewListener(
nodeKeyECDH, listenAddr.String(), nodeKeyECDH, listenAddr.String(),
s.peerAccessMan.checkIncomingConnBanScore, // TODO(yy): remove this check and unify the inbound
// connection check inside `InboundPeerConnected`.
s.peerAccessMan.checkAcceptIncomingConn,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -4009,22 +4028,6 @@ func (s *server) InboundPeerConnected(conn net.Conn) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
// If the remote node's public key is banned, drop the connection.
access, err := s.peerAccessMan.assignPeerPerms(nodePub)
if err != nil {
// Clean up the persistent peer maps if we're dropping this
// connection.
s.bannedPersistentPeerConnection(pubStr)
srvrLog.Debugf("Dropping connection for %x since we are out "+
"of restricted-access connection slots: %v.", pubSer,
err)
conn.Close()
return
}
// If we already have an outbound connection to this peer, then ignore // If we already have an outbound connection to this peer, then ignore
// this new connection. // this new connection.
if p, ok := s.outboundPeers[pubStr]; ok { if p, ok := s.outboundPeers[pubStr]; ok {
@@ -4059,7 +4062,7 @@ func (s *server) InboundPeerConnected(conn net.Conn) {
// We were unable to locate an existing connection with the // We were unable to locate an existing connection with the
// target peer, proceed to connect. // target peer, proceed to connect.
s.cancelConnReqs(pubStr, nil) s.cancelConnReqs(pubStr, nil)
s.peerConnected(conn, nil, true, access) s.peerConnected(conn, nil, true)
case nil: case nil:
// We already have a connection with the incoming peer. If the // We already have a connection with the incoming peer. If the
@@ -4091,7 +4094,7 @@ func (s *server) InboundPeerConnected(conn net.Conn) {
s.removePeer(connectedPeer) s.removePeer(connectedPeer)
s.ignorePeerTermination[connectedPeer] = struct{}{} s.ignorePeerTermination[connectedPeer] = struct{}{}
s.scheduledPeerConnection[pubStr] = func() { s.scheduledPeerConnection[pubStr] = func() {
s.peerConnected(conn, nil, true, access) s.peerConnected(conn, nil, true)
} }
} }
} }
@@ -4116,25 +4119,6 @@ func (s *server) OutboundPeerConnected(connReq *connmgr.ConnReq, conn net.Conn)
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
access, err := s.peerAccessMan.assignPeerPerms(nodePub)
if err != nil {
// Clean up the persistent peer maps if we're dropping this
// connection.
s.bannedPersistentPeerConnection(pubStr)
srvrLog.Debugf("Dropping connection for %x since we are out "+
"of restricted-access connection slots: %v.", pubSer,
err)
if connReq != nil {
s.connMgr.Remove(connReq.ID())
}
conn.Close()
return
}
// If we already have an inbound connection to this peer, then ignore // If we already have an inbound connection to this peer, then ignore
// this new connection. // this new connection.
if p, ok := s.inboundPeers[pubStr]; ok { if p, ok := s.inboundPeers[pubStr]; ok {
@@ -4169,7 +4153,7 @@ func (s *server) OutboundPeerConnected(connReq *connmgr.ConnReq, conn net.Conn)
return return
} }
srvrLog.Infof("Established connection to: %x@%v", pubStr, srvrLog.Infof("Established outbound connection to: %x@%v", pubStr,
conn.RemoteAddr()) conn.RemoteAddr())
if connReq != nil { if connReq != nil {
@@ -4193,7 +4177,7 @@ func (s *server) OutboundPeerConnected(connReq *connmgr.ConnReq, conn net.Conn)
case ErrPeerNotConnected: case ErrPeerNotConnected:
// We were unable to locate an existing connection with the // We were unable to locate an existing connection with the
// target peer, proceed to connect. // target peer, proceed to connect.
s.peerConnected(conn, connReq, false, access) s.peerConnected(conn, connReq, false)
case nil: case nil:
// We already have a connection with the incoming peer. If the // We already have a connection with the incoming peer. If the
@@ -4227,7 +4211,7 @@ func (s *server) OutboundPeerConnected(connReq *connmgr.ConnReq, conn net.Conn)
s.removePeer(connectedPeer) s.removePeer(connectedPeer)
s.ignorePeerTermination[connectedPeer] = struct{}{} s.ignorePeerTermination[connectedPeer] = struct{}{}
s.scheduledPeerConnection[pubStr] = func() { s.scheduledPeerConnection[pubStr] = func() {
s.peerConnected(conn, connReq, false, access) s.peerConnected(conn, connReq, false)
} }
} }
} }
@@ -4304,43 +4288,48 @@ func (s *server) SubscribeCustomMessages() (*subscribe.Client, error) {
// notifyOpenChannelPeerEvent updates the access manager's maps and then calls // notifyOpenChannelPeerEvent updates the access manager's maps and then calls
// the channelNotifier's NotifyOpenChannelEvent. // the channelNotifier's NotifyOpenChannelEvent.
func (s *server) notifyOpenChannelPeerEvent(op wire.OutPoint, func (s *server) notifyOpenChannelPeerEvent(op wire.OutPoint,
remotePub *btcec.PublicKey) error { remotePub *btcec.PublicKey) {
// Call newOpenChan to update the access manager's maps for this peer. // Call newOpenChan to update the access manager's maps for this peer.
if err := s.peerAccessMan.newOpenChan(remotePub); err != nil { if err := s.peerAccessMan.newOpenChan(remotePub); err != nil {
return err srvrLog.Errorf("Failed to update peer[%x] access status after "+
"channel[%v] open", remotePub.SerializeCompressed(), op)
} }
// Notify subscribers about this open channel event. // Notify subscribers about this open channel event.
s.channelNotifier.NotifyOpenChannelEvent(op) s.channelNotifier.NotifyOpenChannelEvent(op)
return nil
} }
// notifyPendingOpenChannelPeerEvent updates the access manager's maps and then // notifyPendingOpenChannelPeerEvent updates the access manager's maps and then
// calls the channelNotifier's NotifyPendingOpenChannelEvent. // calls the channelNotifier's NotifyPendingOpenChannelEvent.
func (s *server) notifyPendingOpenChannelPeerEvent(op wire.OutPoint, func (s *server) notifyPendingOpenChannelPeerEvent(op wire.OutPoint,
pendingChan *channeldb.OpenChannel, remotePub *btcec.PublicKey) error { pendingChan *channeldb.OpenChannel, remotePub *btcec.PublicKey) {
// Call newPendingOpenChan to update the access manager's maps for this // Call newPendingOpenChan to update the access manager's maps for this
// peer. // peer.
if err := s.peerAccessMan.newPendingOpenChan(remotePub); err != nil { if err := s.peerAccessMan.newPendingOpenChan(remotePub); err != nil {
return err srvrLog.Errorf("Failed to update peer[%x] access status after "+
"channel[%v] pending open",
remotePub.SerializeCompressed(), op)
} }
// Notify subscribers about this event. // Notify subscribers about this event.
s.channelNotifier.NotifyPendingOpenChannelEvent(op, pendingChan) s.channelNotifier.NotifyPendingOpenChannelEvent(op, pendingChan)
return nil
} }
// notifyFundingTimeoutPeerEvent updates the access manager's maps and then // notifyFundingTimeoutPeerEvent updates the access manager's maps and then
// calls the channelNotifier's NotifyFundingTimeout. // calls the channelNotifier's NotifyFundingTimeout.
func (s *server) notifyFundingTimeoutPeerEvent(op wire.OutPoint, func (s *server) notifyFundingTimeoutPeerEvent(op wire.OutPoint,
remotePub *btcec.PublicKey) error { remotePub *btcec.PublicKey) {
// Call newPendingCloseChan to potentially demote the peer. // Call newPendingCloseChan to potentially demote the peer.
err := s.peerAccessMan.newPendingCloseChan(remotePub) err := s.peerAccessMan.newPendingCloseChan(remotePub)
if err != nil {
srvrLog.Errorf("Failed to update peer[%x] access status after "+
"channel[%v] pending close",
remotePub.SerializeCompressed(), op)
}
if errors.Is(err, ErrNoMoreRestrictedAccessSlots) { if errors.Is(err, ErrNoMoreRestrictedAccessSlots) {
// If we encounter an error while attempting to disconnect the // If we encounter an error while attempting to disconnect the
// peer, log the error. // peer, log the error.
@@ -4351,8 +4340,6 @@ func (s *server) notifyFundingTimeoutPeerEvent(op wire.OutPoint,
// Notify subscribers about this event. // Notify subscribers about this event.
s.channelNotifier.NotifyFundingTimeout(op) s.channelNotifier.NotifyFundingTimeout(op)
return nil
} }
// peerConnected is a function that handles initialization a newly connected // peerConnected is a function that handles initialization a newly connected
@@ -4360,12 +4347,35 @@ func (s *server) notifyFundingTimeoutPeerEvent(op wire.OutPoint,
// starting all the goroutines the peer needs to function properly. The inbound // starting all the goroutines the peer needs to function properly. The inbound
// boolean should be true if the peer initiated the connection to us. // boolean should be true if the peer initiated the connection to us.
func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq, func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
inbound bool, access peerAccessStatus) { inbound bool) {
brontideConn := conn.(*brontide.Conn) brontideConn := conn.(*brontide.Conn)
addr := conn.RemoteAddr() addr := conn.RemoteAddr()
pubKey := brontideConn.RemotePub() pubKey := brontideConn.RemotePub()
// Only restrict access for inbound connections, which means if the
// remote node's public key is banned or the restricted slots are used
// up, we will drop the connection.
//
// TODO(yy): Consider perform this check in
// `peerAccessMan.addPeerAccess`.
access, err := s.peerAccessMan.assignPeerPerms(pubKey)
if inbound && err != nil {
pubSer := pubKey.SerializeCompressed()
// Clean up the persistent peer maps if we're dropping this
// connection.
s.bannedPersistentPeerConnection(string(pubSer))
srvrLog.Debugf("Dropping connection for %x since we are out "+
"of restricted-access connection slots: %v.", pubSer,
err)
conn.Close()
return
}
srvrLog.Infof("Finalizing connection to %x@%s, inbound=%v", srvrLog.Infof("Finalizing connection to %x@%s, inbound=%v",
pubKey.SerializeCompressed(), addr, inbound) pubKey.SerializeCompressed(), addr, inbound)
@@ -4503,7 +4513,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
p := peer.NewBrontide(pCfg) p := peer.NewBrontide(pCfg)
// Update the access manager with the access permission for this peer. // Update the access manager with the access permission for this peer.
s.peerAccessMan.addPeerAccess(pubKey, access) s.peerAccessMan.addPeerAccess(pubKey, access, inbound)
// TODO(roasbeef): update IP address for link-node // TODO(roasbeef): update IP address for link-node
// * also mark last-seen, do it one single transaction? // * also mark last-seen, do it one single transaction?
@@ -4965,7 +4975,8 @@ func (s *server) removePeer(p *peer.Brontide) {
} }
// Remove the peer's access permission from the access manager. // Remove the peer's access permission from the access manager.
s.peerAccessMan.removePeerAccess(p.IdentityKey()) peerPubStr := string(p.IdentityKey().SerializeCompressed())
s.peerAccessMan.removePeerAccess(peerPubStr)
// Copy the peer's error buffer across to the server if it has any items // Copy the peer's error buffer across to the server if it has any items
// in it so that we can restore peer errors across connections. // in it so that we can restore peer errors across connections.
@@ -4999,7 +5010,11 @@ func (s *server) ConnectToPeer(addr *lnwire.NetAddress,
// Ensure we're not already connected to this peer. // Ensure we're not already connected to this peer.
peer, err := s.findPeerByPubStr(targetPub) peer, err := s.findPeerByPubStr(targetPub)
if err == nil {
// When there's no error it means we already have a connection with this
// peer. If this is a dev environment with the `--unsafeconnect` flag
// set, we will ignore the existing connection and continue.
if err == nil && !s.cfg.Dev.GetUnsafeConnect() {
s.mu.Unlock() s.mu.Unlock()
return &errPeerAlreadyConnected{peer: peer} return &errPeerAlreadyConnected{peer: peer}
} }