Skip to content

Commit 4e70a5d

Browse files
committed
server.go+accessman.go: introduce caches for access permissions
Here we introduce the access manager which has caches that will determine the access control status of our peers. Peers that have had their funding transaction confirm with us are protected. Peers that only have pending-open channels with us are temporary access and can have their access revoked. The rest of the peers are granted restricted access.
1 parent 77908b3 commit 4e70a5d

File tree

10 files changed

+788
-53
lines changed

10 files changed

+788
-53
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)