control/controlclient: fix deadlock in map session change queue processing (#19828)
Holding an exclusive lock while writing to the unbuffered changequeue chan
is likely going to deadlock when the run() path may try to grab the same lock
before reading from the chan to drain it (on map session close). This causes
the client to stop processing new map responses and TSMP disco key advertisements.
There is a good probability of inducing this deadlock using the old code and new
test added in this commit: TestUpdateDiscoForNodeCallback/test_deadlock.
Also fix an unintentional regression in how the client responds to a mapResponse sleep
command. 85bb5f84a5 moved the processing of mapResponses into a new goroutine,
serialized via mapSession's changequeue. Thus, controlclient stopped sleeping in the
same goroutine servicing mapResponses/control connections. This commit brings us back
to sleeping synchronously in the same goroutine as controlclient.
Updates #12639
Signed-off-by: Amal Bansode <amal@tailscale.com>
Signed-off-by: Claus Lensbøl <claus@tailscale.com>
Co-authored-by: Claus Lensbøl <claus@tailscale.com>
This commit is contained in:
co-authored by
Claus Lensbøl
parent
fd2405ca8f
commit
e32b9bde1d
@@ -207,6 +207,7 @@ func (ms *mapSession) updateDiscoForNode(id tailcfg.NodeID, key key.NodePublic,
|
||||
ms.processQueue.Wait()
|
||||
return ErrChangeQueueClosed
|
||||
}
|
||||
defer ms.cqmu.Unlock()
|
||||
|
||||
resp := responseWithSource{
|
||||
response: &tailcfg.MapResponse{
|
||||
@@ -220,12 +221,24 @@ func (ms *mapSession) updateDiscoForNode(id tailcfg.NodeID, key key.NodePublic,
|
||||
},
|
||||
viaTSMP: true,
|
||||
}
|
||||
ms.changeQueue <- resp
|
||||
ms.cqmu.Unlock()
|
||||
return nil
|
||||
return ms.addRespToQueue(resp)
|
||||
}
|
||||
|
||||
// HandleNonKeepAliveMapResponse handles a non-KeepAlive MapResponse (full or
|
||||
// incremental).
|
||||
//
|
||||
// All fields that are valid on a KeepAlive MapResponse have already been
|
||||
// handled.
|
||||
//
|
||||
// Debug messages are handled first, followed by pushing the response onto a
|
||||
// queue for new updates handled sequentially.
|
||||
func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *tailcfg.MapResponse) error {
|
||||
if debug := resp.Debug; debug != nil {
|
||||
if err := ms.onDebug(ctx, debug); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ms.cqmu.Lock()
|
||||
|
||||
if ms.changeQueueClosed {
|
||||
@@ -234,14 +247,23 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
return ErrChangeQueueClosed
|
||||
}
|
||||
|
||||
defer ms.cqmu.Unlock()
|
||||
|
||||
change := responseWithSource{
|
||||
response: resp,
|
||||
viaTSMP: false,
|
||||
}
|
||||
|
||||
ms.changeQueue <- change
|
||||
ms.cqmu.Unlock()
|
||||
return nil
|
||||
return ms.addRespToQueue(change)
|
||||
}
|
||||
|
||||
func (ms *mapSession) addRespToQueue(resp responseWithSource) error {
|
||||
select {
|
||||
case ms.changeQueue <- resp:
|
||||
return nil
|
||||
case <-ms.sessionAliveCtx.Done():
|
||||
return ErrChangeQueueClosed
|
||||
}
|
||||
}
|
||||
|
||||
// handleNonKeepAliveMapResponse handles a non-KeepAlive MapResponse (full or
|
||||
@@ -253,12 +275,6 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
|
||||
// TODO(bradfitz): make this handle all fields later. For now (2023-08-20) this
|
||||
// is [re]factoring progress enough.
|
||||
func (ms *mapSession) handleNonKeepAliveMapResponse(ctx context.Context, resp *tailcfg.MapResponse, viaTSMP bool) error {
|
||||
if debug := resp.Debug; debug != nil {
|
||||
if err := ms.onDebug(ctx, debug); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints() {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
|
||||
Reference in New Issue
Block a user