From 1625e87526100573f9844822d42373788240648d Mon Sep 17 00:00:00 2001 From: Nick O'Neill Date: Wed, 9 Mar 2022 14:42:42 -0800 Subject: [PATCH] control/controlclient, localapi: shorten expiry time via localapi (#4112) Signed-off-by: Nick O'Neill --- control/controlclient/auto.go | 4 ++++ control/controlclient/client.go | 4 ++++ control/controlclient/direct.go | 21 +++++++++++++++++++-- ipn/ipnlocal/local.go | 9 +++++++++ ipn/ipnlocal/state_test.go | 6 ++++++ ipn/localapi/localapi.go | 31 +++++++++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) diff --git a/control/controlclient/auto.go b/control/controlclient/auto.go index 752c8f1fc..292229ca8 100644 --- a/control/controlclient/auto.go +++ b/control/controlclient/auto.go @@ -657,6 +657,10 @@ func (c *Auto) Logout(ctx context.Context) error { } } +func (c *Auto) SetExpirySooner(ctx context.Context, expiry time.Time) error { + return c.direct.SetExpirySooner(ctx, expiry) +} + // UpdateEndpoints sets the client's discovered endpoints and sends // them to the control server if they've changed. // diff --git a/control/controlclient/client.go b/control/controlclient/client.go index 011701d6b..3aff039d2 100644 --- a/control/controlclient/client.go +++ b/control/controlclient/client.go @@ -11,6 +11,7 @@ package controlclient import ( "context" + "time" "tailscale.com/tailcfg" ) @@ -45,6 +46,9 @@ type Client interface { // Logout starts a synchronous logout process. It doesn't return // until the logout operation has been completed. Logout(context.Context) error + // SetExpirySooner sets the node's expiry time via the controlclient, + // as long as it's shorter than the current expiry time. + SetExpirySooner(context.Context, time.Time) error // SetPaused pauses or unpauses the controlclient activity as much // as possible, without losing its internal state, to minimize // unnecessary network activity. diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index baf50efd7..057cffb95 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -302,12 +302,27 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, opt loginOpt) (newURL strin return url, err } +// SetExpirySooner attempts to shorten the expiry to the specified time. +func (c *Direct) SetExpirySooner(ctx context.Context, expiry time.Time) error { + c.logf("[v1] direct.SetExpirySooner()") + + newURL, err := c.doLoginOrRegen(ctx, loginOpt{Expiry: &expiry}) + c.logf("[v1] SetExpirySooner control response: newURL=%v, err=%v", newURL, err) + + return err +} + type loginOpt struct { Token *tailcfg.Oauth2Token Flags LoginFlags - Regen bool + Regen bool // generate a new nodekey, can be overridden in doLogin URL string - Logout bool + Logout bool // set the expiry to the far past, expiring the node + // Expiry, if non-nil, attempts to set the node expiry to the + // specified time and cannot be used to extend the expiry. + // It is ignored if Logout is set since Logout works by setting a + // expiry time in the far past. + Expiry *time.Time } // httpClient provides a common interface for the noiseClient and @@ -406,6 +421,8 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new } if opt.Logout { request.Expiry = time.Unix(123, 0) // far in the past + } else if opt.Expiry != nil { + request.Expiry = *opt.Expiry } c.logf("RegisterReq: onode=%v node=%v fup=%v", request.OldNodeKey.ShortString(), diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 2a263f7c0..890ae65d7 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3187,6 +3187,15 @@ func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool { return true } +// SetExpiry updates the expiry of the current node key to t, as long as it's +// only sooner than the old expiry. +// +// If t is in the past, the key is expired immediately. +// If t is after the current expiry, an error is returned. +func (b *LocalBackend) SetExpirySooner(ctx context.Context, expiry time.Time) error { + return b.cc.SetExpirySooner(ctx, expiry) +} + // exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters // to exitNodeID's DoH service, if available. // diff --git a/ipn/ipnlocal/state_test.go b/ipn/ipnlocal/state_test.go index 6b963e087..8d79cfb2c 100644 --- a/ipn/ipnlocal/state_test.go +++ b/ipn/ipnlocal/state_test.go @@ -223,6 +223,12 @@ func (cc *mockControl) Logout(ctx context.Context) error { return nil } +func (cc *mockControl) SetExpirySooner(context.Context, time.Time) error { + cc.logf("SetExpirySooner") + cc.called("SetExpirySooner") + return nil +} + func (cc *mockControl) SetPaused(paused bool) { cc.logf("SetPaused=%v", paused) if paused { diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index d1da06b74..1bf1e6a68 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -122,6 +122,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveMetrics(w, r) case "/localapi/v0/debug": h.serveDebug(w, r) + case "/localapi/v0/set-expiry-sooner": + h.serveSetExpirySooner(w, r) case "/": io.WriteString(w, "tailscaled\n") default: @@ -511,6 +513,35 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) { e.Encode(h.b.DERPMap()) } +// serveSetExpirySooner sets the expiry date on the current machine, specified +// by an `expiry` unix timestamp as POST or query param. +func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "POST required", http.StatusMethodNotAllowed) + return + } + + var expiryTime time.Time + if v := r.FormValue("expiry"); v != "" { + expiryInt, err := strconv.ParseInt(v, 10, 64) + if err != nil { + http.Error(w, "can't parse expiry time, expects a unix timestamp", http.StatusBadRequest) + return + } + expiryTime = time.Unix(expiryInt, 0) + } else { + http.Error(w, "missing 'expiry' parameter, a unix timestamp", http.StatusBadRequest) + return + } + err := h.b.SetExpirySooner(r.Context(), expiryTime) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "text/plain") + io.WriteString(w, "done\n") +} + func defBool(a string, def bool) bool { if a == "" { return def