Skip to content

Commit 6531d45

Browse files
authored
Merge pull request #9458 from Crypt-iQ/banning_010072025
multi+server.go: add initial permissions for some peers
2 parents 5d8309e + 6309b8a commit 6531d45

File tree

19 files changed

+1274
-63
lines changed

19 files changed

+1274
-63
lines changed

accessman.go

Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
package lnd
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"github.com/btcsuite/btcd/btcec/v2"
8+
"github.com/lightningnetwork/lnd/channeldb"
9+
)
10+
11+
// accessMan is responsible for managing the server's access permissions.
12+
type accessMan struct {
13+
cfg *accessManConfig
14+
15+
// banScoreMtx is used for the server's ban tracking. If the server
16+
// mutex is also going to be locked, ensure that this is locked after
17+
// the server mutex.
18+
banScoreMtx sync.RWMutex
19+
20+
// peerCounts is a mapping from remote public key to {bool, uint64}
21+
// where the bool indicates that we have an open/closed channel with
22+
// the peer and where the uint64 indicates the number of pending-open
23+
// channels we currently have with them. This mapping will be used to
24+
// determine access permissions for the peer. The map key is the
25+
// string-version of the serialized public key.
26+
//
27+
// NOTE: This MUST be accessed with the banScoreMtx held.
28+
peerCounts map[string]channeldb.ChanCount
29+
30+
// peerScores stores each connected peer's access status. The map key
31+
// is the string-version of the serialized public key.
32+
//
33+
// NOTE: This MUST be accessed with the banScoreMtx held.
34+
peerScores map[string]peerSlotStatus
35+
36+
// numRestricted tracks the number of peers with restricted access in
37+
// peerScores. This MUST be accessed with the banScoreMtx held.
38+
numRestricted int64
39+
}
40+
41+
type accessManConfig struct {
42+
// initAccessPerms checks the channeldb for initial access permissions
43+
// and then populates the peerCounts and peerScores maps.
44+
initAccessPerms func() (map[string]channeldb.ChanCount, error)
45+
46+
// shouldDisconnect determines whether we should disconnect a peer or
47+
// not.
48+
shouldDisconnect func(*btcec.PublicKey) (bool, error)
49+
50+
// maxRestrictedSlots is the number of restricted slots we'll allocate.
51+
maxRestrictedSlots int64
52+
}
53+
54+
func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
55+
a := &accessMan{
56+
cfg: cfg,
57+
peerCounts: make(map[string]channeldb.ChanCount),
58+
peerScores: make(map[string]peerSlotStatus),
59+
}
60+
61+
counts, err := a.cfg.initAccessPerms()
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
// We'll populate the server's peerCounts map with the counts fetched
67+
// via initAccessPerms. Also note that we haven't yet connected to the
68+
// peers.
69+
for peerPub, count := range counts {
70+
a.peerCounts[peerPub] = count
71+
}
72+
73+
return a, nil
74+
}
75+
76+
// assignPeerPerms assigns a new peer its permissions. This does not track the
77+
// access in the maps. This is intentional.
78+
func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
79+
peerAccessStatus, error) {
80+
81+
// Default is restricted unless the below filters say otherwise.
82+
access := peerStatusRestricted
83+
84+
shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
85+
if err != nil {
86+
// Access is restricted here.
87+
return access, err
88+
}
89+
90+
if shouldDisconnect {
91+
// Access is restricted here.
92+
return access, ErrGossiperBan
93+
}
94+
95+
peerMapKey := string(remotePub.SerializeCompressed())
96+
97+
// Lock banScoreMtx for reading so that we can update the banning maps
98+
// below.
99+
a.banScoreMtx.RLock()
100+
defer a.banScoreMtx.RUnlock()
101+
102+
if count, found := a.peerCounts[peerMapKey]; found {
103+
if count.HasOpenOrClosedChan {
104+
access = peerStatusProtected
105+
} else if count.PendingOpenCount != 0 {
106+
access = peerStatusTemporary
107+
}
108+
}
109+
110+
// If we've reached this point and access hasn't changed from
111+
// restricted, then we need to check if we even have a slot for this
112+
// peer.
113+
if a.numRestricted >= a.cfg.maxRestrictedSlots &&
114+
access == peerStatusRestricted {
115+
116+
return access, ErrNoMoreRestrictedAccessSlots
117+
}
118+
119+
return access, nil
120+
}
121+
122+
// newPendingOpenChan is called after the pending-open channel has been
123+
// committed to the database. This may transition a restricted-access peer to a
124+
// temporary-access peer.
125+
func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
126+
a.banScoreMtx.Lock()
127+
defer a.banScoreMtx.Unlock()
128+
129+
peerMapKey := string(remotePub.SerializeCompressed())
130+
131+
// Fetch the peer's access status from peerScores.
132+
status, found := a.peerScores[peerMapKey]
133+
if !found {
134+
// If we didn't find the peer, we'll return an error.
135+
return ErrNoPeerScore
136+
}
137+
138+
switch status.state {
139+
case peerStatusProtected:
140+
// If this peer's access status is protected, we don't need to
141+
// do anything.
142+
return nil
143+
144+
case peerStatusTemporary:
145+
// If this peer's access status is temporary, we'll need to
146+
// update the peerCounts map. The peer's access status will
147+
// stay temporary.
148+
peerCount, found := a.peerCounts[peerMapKey]
149+
if !found {
150+
// Error if we did not find any info in peerCounts.
151+
return ErrNoPendingPeerInfo
152+
}
153+
154+
// Increment the pending channel amount.
155+
peerCount.PendingOpenCount += 1
156+
a.peerCounts[peerMapKey] = peerCount
157+
158+
case peerStatusRestricted:
159+
// If the peer's access status is restricted, then we can
160+
// transition it to a temporary-access peer. We'll need to
161+
// update numRestricted and also peerScores. We'll also need to
162+
// update peerCounts.
163+
peerCount := channeldb.ChanCount{
164+
HasOpenOrClosedChan: false,
165+
PendingOpenCount: 1,
166+
}
167+
168+
a.peerCounts[peerMapKey] = peerCount
169+
170+
// A restricted-access slot has opened up.
171+
a.numRestricted -= 1
172+
173+
a.peerScores[peerMapKey] = peerSlotStatus{
174+
state: peerStatusTemporary,
175+
}
176+
177+
default:
178+
// This should not be possible.
179+
return fmt.Errorf("invalid peer access status")
180+
}
181+
182+
return nil
183+
}
184+
185+
// newPendingCloseChan is called when a pending-open channel prematurely closes
186+
// before the funding transaction has confirmed. This potentially demotes a
187+
// temporary-access peer to a restricted-access peer. If no restricted-access
188+
// slots are available, the peer will be disconnected.
189+
func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
190+
a.banScoreMtx.Lock()
191+
defer a.banScoreMtx.Unlock()
192+
193+
peerMapKey := string(remotePub.SerializeCompressed())
194+
195+
// Fetch the peer's access status from peerScores.
196+
status, found := a.peerScores[peerMapKey]
197+
if !found {
198+
return ErrNoPeerScore
199+
}
200+
201+
switch status.state {
202+
case peerStatusProtected:
203+
// If this peer is protected, we don't do anything.
204+
return nil
205+
206+
case peerStatusTemporary:
207+
// If this peer is temporary, we need to check if it will
208+
// revert to a restricted-access peer.
209+
peerCount, found := a.peerCounts[peerMapKey]
210+
if !found {
211+
// Error if we did not find any info in peerCounts.
212+
return ErrNoPendingPeerInfo
213+
}
214+
215+
currentNumPending := peerCount.PendingOpenCount - 1
216+
if currentNumPending == 0 {
217+
// Remove the entry from peerCounts.
218+
delete(a.peerCounts, peerMapKey)
219+
220+
// If this is the only pending-open channel for this
221+
// peer and it's getting removed, attempt to demote
222+
// this peer to a restricted peer.
223+
if a.numRestricted == a.cfg.maxRestrictedSlots {
224+
// There are no available restricted slots, so
225+
// we need to disconnect this peer. We leave
226+
// this up to the caller.
227+
return ErrNoMoreRestrictedAccessSlots
228+
}
229+
230+
// Otherwise, there is an available restricted-access
231+
// slot, so we can demote this peer.
232+
a.peerScores[peerMapKey] = peerSlotStatus{
233+
state: peerStatusRestricted,
234+
}
235+
236+
// Update numRestricted.
237+
a.numRestricted++
238+
239+
return nil
240+
}
241+
242+
// Else, we don't need to demote this peer since it has other
243+
// pending-open channels with us.
244+
peerCount.PendingOpenCount = currentNumPending
245+
a.peerCounts[peerMapKey] = peerCount
246+
247+
return nil
248+
249+
case peerStatusRestricted:
250+
// This should not be possible. This indicates an error.
251+
return fmt.Errorf("invalid peer access state transition")
252+
253+
default:
254+
// This should not be possible.
255+
return fmt.Errorf("invalid peer access status")
256+
}
257+
}
258+
259+
// newOpenChan is called when a pending-open channel becomes an open channel
260+
// (i.e. the funding transaction has confirmed). If the remote peer is a
261+
// temporary-access peer, it will be promoted to a protected-access peer.
262+
func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
263+
a.banScoreMtx.Lock()
264+
defer a.banScoreMtx.Unlock()
265+
266+
peerMapKey := string(remotePub.SerializeCompressed())
267+
268+
// Fetch the peer's access status from peerScores.
269+
status, found := a.peerScores[peerMapKey]
270+
if !found {
271+
// If we didn't find the peer, we'll return an error.
272+
return ErrNoPeerScore
273+
}
274+
275+
switch status.state {
276+
case peerStatusProtected:
277+
// If the peer's state is already protected, we don't need to
278+
// do anything more.
279+
return nil
280+
281+
case peerStatusTemporary:
282+
// If the peer's state is temporary, we'll upgrade the peer to
283+
// a protected peer.
284+
peerCount, found := a.peerCounts[peerMapKey]
285+
if !found {
286+
// Error if we did not find any info in peerCounts.
287+
return ErrNoPendingPeerInfo
288+
}
289+
290+
peerCount.HasOpenOrClosedChan = true
291+
a.peerCounts[peerMapKey] = peerCount
292+
293+
newStatus := peerSlotStatus{
294+
state: peerStatusProtected,
295+
}
296+
a.peerScores[peerMapKey] = newStatus
297+
298+
return nil
299+
300+
case peerStatusRestricted:
301+
// This should not be possible. For the server to receive a
302+
// state-transition event via NewOpenChan, the server must have
303+
// previously granted this peer "temporary" access. This
304+
// temporary access would not have been revoked or downgraded
305+
// without `CloseChannel` being called with the pending
306+
// argument set to true. This means that an open-channel state
307+
// transition would be impossible. Therefore, we can return an
308+
// error.
309+
return fmt.Errorf("invalid peer access status")
310+
311+
default:
312+
// This should not be possible.
313+
return fmt.Errorf("invalid peer access status")
314+
}
315+
}
316+
317+
// checkIncomingConnBanScore checks whether, given the remote's public hex-
318+
// encoded key, we should not accept this incoming connection or immediately
319+
// disconnect. This does not assign to the server's peerScores maps. This is
320+
// just an inbound filter that the brontide listeners use.
321+
func (a *accessMan) checkIncomingConnBanScore(remotePub *btcec.PublicKey) (
322+
bool, error) {
323+
324+
a.banScoreMtx.RLock()
325+
defer a.banScoreMtx.RUnlock()
326+
327+
peerMapKey := string(remotePub.SerializeCompressed())
328+
329+
if _, found := a.peerCounts[peerMapKey]; !found {
330+
// Check numRestricted to see if there is an available slot. In
331+
// the future, it's possible to add better heuristics.
332+
if a.numRestricted < a.cfg.maxRestrictedSlots {
333+
// There is an available slot.
334+
return true, nil
335+
}
336+
337+
// If there are no slots left, then we reject this connection.
338+
return false, ErrNoMoreRestrictedAccessSlots
339+
}
340+
341+
// Else, the peer is either protected or temporary.
342+
return true, nil
343+
}
344+
345+
// addPeerAccess tracks a peer's access in the maps. This should be called when
346+
// the peer has fully connected.
347+
func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
348+
access peerAccessStatus) {
349+
350+
// Add the remote public key to peerScores.
351+
a.banScoreMtx.Lock()
352+
defer a.banScoreMtx.Unlock()
353+
354+
peerMapKey := string(remotePub.SerializeCompressed())
355+
356+
a.peerScores[peerMapKey] = peerSlotStatus{state: access}
357+
358+
// Increment numRestricted.
359+
if access == peerStatusRestricted {
360+
a.numRestricted++
361+
}
362+
}
363+
364+
// removePeerAccess removes the peer's access from the maps. This should be
365+
// called when the peer has been disconnected.
366+
func (a *accessMan) removePeerAccess(remotePub *btcec.PublicKey) {
367+
a.banScoreMtx.Lock()
368+
defer a.banScoreMtx.Unlock()
369+
370+
peerMapKey := string(remotePub.SerializeCompressed())
371+
372+
status, found := a.peerScores[peerMapKey]
373+
if !found {
374+
return
375+
}
376+
377+
if status.state == peerStatusRestricted {
378+
// If the status is restricted, then we decrement from
379+
// numRestrictedSlots.
380+
a.numRestricted--
381+
}
382+
383+
delete(a.peerScores, peerMapKey)
384+
}

0 commit comments

Comments
 (0)