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