watchtower: prevent removal of last tower addr

This addresses a potential panic when a tower has one of its candidate
sessions chosen, but its only reachable address was removed by a
user-initiated RPC before the fact.
This commit is contained in:
Wilmer Paulino 2020-11-03 10:27:57 -08:00
parent 3ae46d81f4
commit d3157bcaf8
No known key found for this signature in database
GPG Key ID: 6DF57B9F9514972F
5 changed files with 28 additions and 5 deletions

@ -19,7 +19,7 @@ type TowerCandidateIterator interface {
// iterator. An optional address can be provided to indicate a stale // iterator. An optional address can be provided to indicate a stale
// tower address to remove it. If it isn't provided, then the tower is // tower address to remove it. If it isn't provided, then the tower is
// completely removed from the iterator. // completely removed from the iterator.
RemoveCandidate(wtdb.TowerID, net.Addr) RemoveCandidate(wtdb.TowerID, net.Addr) error
// IsActive determines whether a given tower is exists within the // IsActive determines whether a given tower is exists within the
// iterator. // iterator.
@ -131,19 +131,26 @@ func (t *towerListIterator) AddCandidate(candidate *wtdb.Tower) {
// optional address can be provided to indicate a stale tower address to remove // optional address can be provided to indicate a stale tower address to remove
// it. If it isn't provided, then the tower is completely removed from the // it. If it isn't provided, then the tower is completely removed from the
// iterator. // iterator.
func (t *towerListIterator) RemoveCandidate(candidate wtdb.TowerID, addr net.Addr) { func (t *towerListIterator) RemoveCandidate(candidate wtdb.TowerID,
addr net.Addr) error {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
tower, ok := t.candidates[candidate] tower, ok := t.candidates[candidate]
if !ok { if !ok {
return return nil
} }
if addr != nil { if addr != nil {
tower.RemoveAddress(addr) tower.RemoveAddress(addr)
if len(tower.Addresses) == 0 {
return wtdb.ErrLastTowerAddr
}
} else { } else {
delete(t.candidates, candidate) delete(t.candidates, candidate)
} }
return nil
} }
// IsActive determines whether a given tower is exists within the iterator. // IsActive determines whether a given tower is exists within the iterator.

@ -1124,7 +1124,10 @@ func (c *TowerClient) handleStaleTower(msg *staleTowerMsg) error {
if err := c.cfg.DB.RemoveTower(msg.pubKey, msg.addr); err != nil { if err := c.cfg.DB.RemoveTower(msg.pubKey, msg.addr); err != nil {
return err return err
} }
c.candidateTowers.RemoveCandidate(tower.ID, msg.addr) err = c.candidateTowers.RemoveCandidate(tower.ID, msg.addr)
if err != nil {
return err
}
// If an address was provided, then we're only meant to remove the // If an address was provided, then we're only meant to remove the
// address from the tower, so there's nothing left for us to do. // address from the tower, so there's nothing left for us to do.

@ -108,6 +108,10 @@ var (
// created because session key index differs from the reserved key // created because session key index differs from the reserved key
// index. // index.
ErrIncorrectKeyIndex = errors.New("incorrect key index") ErrIncorrectKeyIndex = errors.New("incorrect key index")
// ErrLastTowerAddr is an error returned when the last address of a
// watchtower is attempted to be removed.
ErrLastTowerAddr = errors.New("cannot remove last tower address")
) )
// ClientDB is single database providing a persistent storage engine for the // ClientDB is single database providing a persistent storage engine for the
@ -333,7 +337,13 @@ func (c *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
if err != nil { if err != nil {
return err return err
} }
// Towers should always have at least one address saved.
tower.RemoveAddress(addr) tower.RemoveAddress(addr)
if len(tower.Addresses) == 0 {
return ErrLastTowerAddr
}
return putTower(towers, tower) return putTower(towers, tower)
} }

@ -445,7 +445,7 @@ func testRemoveTower(h *clientDBHarness) {
// We'll then remove the first address. We should now see that the tower // We'll then remove the first address. We should now see that the tower
// has no addresses left. // has no addresses left.
h.removeTower(pk, addr1, false, nil) h.removeTower(pk, addr1, false, wtdb.ErrLastTowerAddr)
// Removing the tower as a whole from the database should succeed since // Removing the tower as a whole from the database should succeed since
// there aren't any active sessions for it. // there aren't any active sessions for it.

@ -101,6 +101,9 @@ func (m *ClientDB) RemoveTower(pubKey *btcec.PublicKey, addr net.Addr) error {
if addr != nil { if addr != nil {
tower.RemoveAddress(addr) tower.RemoveAddress(addr)
if len(tower.Addresses) == 0 {
return wtdb.ErrLastTowerAddr
}
m.towers[tower.ID] = tower m.towers[tower.ID] = tower
return nil return nil
} }