Skip to content

Commit 2c8859c

Browse files
authored
client/tailscale,ipn/{ipnlocal,localapi}: add a pre-shutdown localAPI endpoint that terminates control connections. (tailscale#14028)
Adds a /disconnect-control local API endpoint that just shuts down control client. This can be run before shutting down an HA subnet router/app connector replica - it will ensure that all connection to control are dropped and control thus considers this node inactive and tells peers to switch over to another replica. Meanwhile the existing connections keep working (assuming that the replica is given some graceful shutdown period). Updates tailscale#14020 Signed-off-by: Irbe Krumina <[email protected]>
1 parent 3090461 commit 2c8859c

File tree

3 files changed

+41
-0
lines changed

3 files changed

+41
-0
lines changed

client/tailscale/localclient.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,17 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
13271327
return nil
13281328
}
13291329

1330+
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
1331+
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
1332+
// to another replica whilst there is still some grace period for the existing connections to terminate.
1333+
func (lc *LocalClient) DisconnectControl(ctx context.Context) error {
1334+
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
1335+
if err != nil {
1336+
return fmt.Errorf("error disconnecting control: %w", err)
1337+
}
1338+
return nil
1339+
}
1340+
13301341
// NetworkLockDisable shuts down network-lock across the tailnet.
13311342
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
13321343
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {

ipn/ipnlocal/local.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,19 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
800800
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
801801
}
802802

803+
// DisconnectControl shuts down control client. This can be run before node shutdown to force control to consider this ndoe
804+
// inactive. This can be used to ensure that nodes that are HA subnet router or app connector replicas are shutting
805+
// down, clients switch over to other replicas whilst the existing connections are kept alive for some period of time.
806+
func (b *LocalBackend) DisconnectControl() {
807+
b.mu.Lock()
808+
defer b.mu.Unlock()
809+
cc := b.resetControlClientLocked()
810+
if cc == nil {
811+
return
812+
}
813+
cc.Shutdown()
814+
}
815+
803816
// captivePortalDetectionInterval is the duration to wait in an unhealthy state with connectivity broken
804817
// before running captive portal detection.
805818
const captivePortalDetectionInterval = 2 * time.Second

ipn/localapi/localapi.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ var handler = map[string]localAPIHandler{
100100
"derpmap": (*Handler).serveDERPMap,
101101
"dev-set-state-store": (*Handler).serveDevSetStateStore,
102102
"dial": (*Handler).serveDial,
103+
"disconnect-control": (*Handler).disconnectControl,
103104
"dns-osconfig": (*Handler).serveDNSOSConfig,
104105
"dns-query": (*Handler).serveDNSQuery,
105106
"drive/fileserver-address": (*Handler).serveDriveServerAddr,
@@ -952,6 +953,22 @@ func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
952953
servePprofFunc(w, r)
953954
}
954955

956+
// disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that
957+
// node no longer communicates with control. Doing this makes control consider this node inactive. This can be used
958+
// before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the
959+
// peers to switch over to another replica whilst still maintaining th existing peer connections.
960+
func (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {
961+
if !h.PermitWrite {
962+
http.Error(w, "access denied", http.StatusForbidden)
963+
return
964+
}
965+
if r.Method != httpm.POST {
966+
http.Error(w, "use POST", http.StatusMethodNotAllowed)
967+
return
968+
}
969+
h.b.DisconnectControl()
970+
}
971+
955972
func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
956973
if !h.PermitWrite {
957974
http.Error(w, "access denied", http.StatusForbidden)

0 commit comments

Comments
 (0)