• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13813542522

12 Mar 2025 02:14PM UTC coverage: 68.647% (+10.3%) from 58.305%
13813542522

push

github

web-flow
Merge pull request #9458 from Crypt-iQ/banning_010072025

multi+server.go: add initial permissions for some peers

397 of 549 new or added lines in 10 files covered. (72.31%)

12 existing lines in 3 files now uncovered.

130390 of 189942 relevant lines covered (68.65%)

23550.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.24
/accessman.go
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) {
4✔
55
        a := &accessMan{
4✔
56
                cfg:        cfg,
4✔
57
                peerCounts: make(map[string]channeldb.ChanCount),
4✔
58
                peerScores: make(map[string]peerSlotStatus),
4✔
59
        }
4✔
60

4✔
61
        counts, err := a.cfg.initAccessPerms()
4✔
62
        if err != nil {
4✔
NEW
63
                return nil, err
×
NEW
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 {
10✔
70
                a.peerCounts[peerPub] = count
6✔
71
        }
6✔
72

73
        return a, nil
4✔
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) {
8✔
80

8✔
81
        // Default is restricted unless the below filters say otherwise.
8✔
82
        access := peerStatusRestricted
8✔
83

8✔
84
        shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
8✔
85
        if err != nil {
8✔
NEW
86
                // Access is restricted here.
×
NEW
87
                return access, err
×
NEW
88
        }
×
89

90
        if shouldDisconnect {
8✔
NEW
91
                // Access is restricted here.
×
NEW
92
                return access, ErrGossiperBan
×
NEW
93
        }
×
94

95
        peerMapKey := string(remotePub.SerializeCompressed())
8✔
96

8✔
97
        // Lock banScoreMtx for reading so that we can update the banning maps
8✔
98
        // below.
8✔
99
        a.banScoreMtx.RLock()
8✔
100
        defer a.banScoreMtx.RUnlock()
8✔
101

8✔
102
        if count, found := a.peerCounts[peerMapKey]; found {
14✔
103
                if count.HasOpenOrClosedChan {
11✔
104
                        access = peerStatusProtected
5✔
105
                } else if count.PendingOpenCount != 0 {
13✔
106
                        access = peerStatusTemporary
4✔
107
                }
4✔
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 &&
8✔
114
                access == peerStatusRestricted {
8✔
NEW
115

×
NEW
116
                return access, ErrNoMoreRestrictedAccessSlots
×
NEW
117
        }
×
118

119
        return access, nil
8✔
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 {
4✔
126
        a.banScoreMtx.Lock()
4✔
127
        defer a.banScoreMtx.Unlock()
4✔
128

4✔
129
        peerMapKey := string(remotePub.SerializeCompressed())
4✔
130

4✔
131
        // Fetch the peer's access status from peerScores.
4✔
132
        status, found := a.peerScores[peerMapKey]
4✔
133
        if !found {
4✔
NEW
134
                // If we didn't find the peer, we'll return an error.
×
NEW
135
                return ErrNoPeerScore
×
NEW
136
        }
×
137

138
        switch status.state {
4✔
139
        case peerStatusProtected:
3✔
140
                // If this peer's access status is protected, we don't need to
3✔
141
                // do anything.
3✔
142
                return nil
3✔
143

144
        case peerStatusTemporary:
3✔
145
                // If this peer's access status is temporary, we'll need to
3✔
146
                // update the peerCounts map. The peer's access status will
3✔
147
                // stay temporary.
3✔
148
                peerCount, found := a.peerCounts[peerMapKey]
3✔
149
                if !found {
3✔
NEW
150
                        // Error if we did not find any info in peerCounts.
×
NEW
151
                        return ErrNoPendingPeerInfo
×
NEW
152
                }
×
153

154
                // Increment the pending channel amount.
155
                peerCount.PendingOpenCount += 1
3✔
156
                a.peerCounts[peerMapKey] = peerCount
3✔
157

158
        case peerStatusRestricted:
4✔
159
                // If the peer's access status is restricted, then we can
4✔
160
                // transition it to a temporary-access peer. We'll need to
4✔
161
                // update numRestricted and also peerScores. We'll also need to
4✔
162
                // update peerCounts.
4✔
163
                peerCount := channeldb.ChanCount{
4✔
164
                        HasOpenOrClosedChan: false,
4✔
165
                        PendingOpenCount:    1,
4✔
166
                }
4✔
167

4✔
168
                a.peerCounts[peerMapKey] = peerCount
4✔
169

4✔
170
                // A restricted-access slot has opened up.
4✔
171
                a.numRestricted -= 1
4✔
172

4✔
173
                a.peerScores[peerMapKey] = peerSlotStatus{
4✔
174
                        state: peerStatusTemporary,
4✔
175
                }
4✔
176

NEW
177
        default:
×
NEW
178
                // This should not be possible.
×
NEW
179
                return fmt.Errorf("invalid peer access status")
×
180
        }
181

182
        return nil
4✔
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 {
4✔
190
        a.banScoreMtx.Lock()
4✔
191
        defer a.banScoreMtx.Unlock()
4✔
192

4✔
193
        peerMapKey := string(remotePub.SerializeCompressed())
4✔
194

4✔
195
        // Fetch the peer's access status from peerScores.
4✔
196
        status, found := a.peerScores[peerMapKey]
4✔
197
        if !found {
4✔
NEW
198
                return ErrNoPeerScore
×
NEW
199
        }
×
200

201
        switch status.state {
4✔
NEW
202
        case peerStatusProtected:
×
NEW
203
                // If this peer is protected, we don't do anything.
×
NEW
204
                return nil
×
205

206
        case peerStatusTemporary:
4✔
207
                // If this peer is temporary, we need to check if it will
4✔
208
                // revert to a restricted-access peer.
4✔
209
                peerCount, found := a.peerCounts[peerMapKey]
4✔
210
                if !found {
4✔
NEW
211
                        // Error if we did not find any info in peerCounts.
×
NEW
212
                        return ErrNoPendingPeerInfo
×
NEW
213
                }
×
214

215
                currentNumPending := peerCount.PendingOpenCount - 1
4✔
216
                if currentNumPending == 0 {
8✔
217
                        // Remove the entry from peerCounts.
4✔
218
                        delete(a.peerCounts, peerMapKey)
4✔
219

4✔
220
                        // If this is the only pending-open channel for this
4✔
221
                        // peer and it's getting removed, attempt to demote
4✔
222
                        // this peer to a restricted peer.
4✔
223
                        if a.numRestricted == a.cfg.maxRestrictedSlots {
5✔
224
                                // There are no available restricted slots, so
1✔
225
                                // we need to disconnect this peer. We leave
1✔
226
                                // this up to the caller.
1✔
227
                                return ErrNoMoreRestrictedAccessSlots
1✔
228
                        }
1✔
229

230
                        // Otherwise, there is an available restricted-access
231
                        // slot, so we can demote this peer.
232
                        a.peerScores[peerMapKey] = peerSlotStatus{
3✔
233
                                state: peerStatusRestricted,
3✔
234
                        }
3✔
235

3✔
236
                        // Update numRestricted.
3✔
237
                        a.numRestricted++
3✔
238

3✔
239
                        return nil
3✔
240
                }
241

242
                // Else, we don't need to demote this peer since it has other
243
                // pending-open channels with us.
NEW
244
                peerCount.PendingOpenCount = currentNumPending
×
NEW
245
                a.peerCounts[peerMapKey] = peerCount
×
NEW
246

×
NEW
247
                return nil
×
248

NEW
249
        case peerStatusRestricted:
×
NEW
250
                // This should not be possible. This indicates an error.
×
NEW
251
                return fmt.Errorf("invalid peer access state transition")
×
252

NEW
253
        default:
×
NEW
254
                // This should not be possible.
×
NEW
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 {
4✔
263
        a.banScoreMtx.Lock()
4✔
264
        defer a.banScoreMtx.Unlock()
4✔
265

4✔
266
        peerMapKey := string(remotePub.SerializeCompressed())
4✔
267

4✔
268
        // Fetch the peer's access status from peerScores.
4✔
269
        status, found := a.peerScores[peerMapKey]
4✔
270
        if !found {
7✔
271
                // If we didn't find the peer, we'll return an error.
3✔
272
                return ErrNoPeerScore
3✔
273
        }
3✔
274

275
        switch status.state {
4✔
276
        case peerStatusProtected:
3✔
277
                // If the peer's state is already protected, we don't need to
3✔
278
                // do anything more.
3✔
279
                return nil
3✔
280

281
        case peerStatusTemporary:
4✔
282
                // If the peer's state is temporary, we'll upgrade the peer to
4✔
283
                // a protected peer.
4✔
284
                peerCount, found := a.peerCounts[peerMapKey]
4✔
285
                if !found {
4✔
NEW
286
                        // Error if we did not find any info in peerCounts.
×
NEW
287
                        return ErrNoPendingPeerInfo
×
NEW
288
                }
×
289

290
                peerCount.HasOpenOrClosedChan = true
4✔
291
                a.peerCounts[peerMapKey] = peerCount
4✔
292

4✔
293
                newStatus := peerSlotStatus{
4✔
294
                        state: peerStatusProtected,
4✔
295
                }
4✔
296
                a.peerScores[peerMapKey] = newStatus
4✔
297

4✔
298
                return nil
4✔
299

NEW
300
        case peerStatusRestricted:
×
NEW
301
                // This should not be possible. For the server to receive a
×
NEW
302
                // state-transition event via NewOpenChan, the server must have
×
NEW
303
                // previously granted this peer "temporary" access. This
×
NEW
304
                // temporary access would not have been revoked or downgraded
×
NEW
305
                // without `CloseChannel` being called with the pending
×
NEW
306
                // argument set to true. This means that an open-channel state
×
NEW
307
                // transition would be impossible. Therefore, we can return an
×
NEW
308
                // error.
×
NEW
309
                return fmt.Errorf("invalid peer access status")
×
310

NEW
311
        default:
×
NEW
312
                // This should not be possible.
×
NEW
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) {
8✔
323

8✔
324
        a.banScoreMtx.RLock()
8✔
325
        defer a.banScoreMtx.RUnlock()
8✔
326

8✔
327
        peerMapKey := string(remotePub.SerializeCompressed())
8✔
328

8✔
329
        if _, found := a.peerCounts[peerMapKey]; !found {
13✔
330
                // Check numRestricted to see if there is an available slot. In
5✔
331
                // the future, it's possible to add better heuristics.
5✔
332
                if a.numRestricted < a.cfg.maxRestrictedSlots {
10✔
333
                        // There is an available slot.
5✔
334
                        return true, nil
5✔
335
                }
5✔
336

337
                // If there are no slots left, then we reject this connection.
338
                return false, ErrNoMoreRestrictedAccessSlots
3✔
339
        }
340

341
        // Else, the peer is either protected or temporary.
342
        return true, nil
6✔
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) {
8✔
349

8✔
350
        // Add the remote public key to peerScores.
8✔
351
        a.banScoreMtx.Lock()
8✔
352
        defer a.banScoreMtx.Unlock()
8✔
353

8✔
354
        peerMapKey := string(remotePub.SerializeCompressed())
8✔
355

8✔
356
        a.peerScores[peerMapKey] = peerSlotStatus{state: access}
8✔
357

8✔
358
        // Increment numRestricted.
8✔
359
        if access == peerStatusRestricted {
13✔
360
                a.numRestricted++
5✔
361
        }
5✔
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) {
3✔
367
        a.banScoreMtx.Lock()
3✔
368
        defer a.banScoreMtx.Unlock()
3✔
369

3✔
370
        peerMapKey := string(remotePub.SerializeCompressed())
3✔
371

3✔
372
        status, found := a.peerScores[peerMapKey]
3✔
373
        if !found {
3✔
NEW
374
                return
×
NEW
375
        }
×
376

377
        if status.state == peerStatusRestricted {
6✔
378
                // If the status is restricted, then we decrement from
3✔
379
                // numRestrictedSlots.
3✔
380
                a.numRestricted--
3✔
381
        }
3✔
382

383
        delete(a.peerScores, peerMapKey)
3✔
384
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc