From f45a9e291b933dfa343dd0ed3abe23fdeb423746 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 4 Mar 2021 20:54:44 -0800 Subject: [PATCH] tailcfg, control/controlclient: add MapResponse.PingRequest So the control server can test whether a client's actually present. Most clients are over HTTP/2, so these pings (to the same host) are super cheap. This mimics the earlier goroutine dump mechanism. Signed-off-by: Brad Fitzpatrick --- control/controlclient/direct.go | 30 ++++++++++++++++++++++++++++++ tailcfg/tailcfg.go | 25 ++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 3e77d0a99..3a7b86787 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -676,6 +676,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm return err } + if pr := resp.PingRequest; pr != nil { + go answerPing(c.logf, c.httpc, pr) + } + if resp.KeepAlive { vlogf("netmap: got keep-alive") } else { @@ -1204,3 +1208,29 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool { return false } + +func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) { + if pr.URL == "" { + logf("invalid PingRequest with no URL") + return + } + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil) + if err != nil { + logf("http.NewRequestWithContext(%q): %v", pr.URL, err) + return + } + if pr.Log { + logf("answerPing: sending ping to %v ...", pr.URL) + } + t0 := time.Now() + _, err = c.Do(req) + d := time.Since(t0).Round(time.Millisecond) + if err != nil { + logf("answerPing error: %v to %v (after %v)", err, pr.URL, d) + } else if pr.Log { + logf("answerPing complete to %v (after %v)", pr.URL, d) + } +} diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index f9b9d57a0..bd61403be 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -35,6 +35,7 @@ import ( // 9: 2020-12-30: client doesn't auto-add implicit search domains from peers; only DNSConfig.Domains // 10: 2021-01-17: client understands MapResponse.PeerSeenChange // 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping +// 12: 2021-03-04: client understands PingRequest const CurrentMapRequestVersion = 11 type StableID string @@ -714,8 +715,30 @@ type DNSConfig struct { Proxied bool } +// PingRequest is a request to send an HTTP request to prove the +// long-polling client is still connected. +type PingRequest struct { + // URL is the URL to send a HEAD request to. + // It will be a unique URL each time. No auth headers are necessary. + URL string + + // Log is whether to log about this ping in the success case. + // For failure cases, the client will log regardless. + Log bool `json:",omitempty"` +} + type MapResponse struct { - KeepAlive bool `json:",omitempty"` // if set, all other fields are ignored + // KeepAlive, if set, represents an empty message just to keep + // the connection alive. When true, all other fields except + // PingRequestURL are ignored. + KeepAlive bool `json:",omitempty"` + + // PingRequest, if non-empty, is a request to the client to + // prove it's still there by sending an HTTP request to the + // provided URL. No auth headers are necessary. + // PingRequest may be sent on any MapResponse (ones with + // KeepAlive true or false). + PingRequest *PingRequest `json:",omitempty"` // Networking Node *Node