diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index bd7579078..4d38e7a15 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -1020,6 +1020,15 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin return decodeJSON[*ipnstate.DebugDERPRegionReport](body) } +// DebugSetExpireIn marks the current node key to expire in d. +// +// This is meant primarily for debug and testing. +func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) error { + v := url.Values{"expiry": {fmt.Sprint(time.Now().Add(d).Unix())}} + _, err := lc.send(ctx, "POST", "/localapi/v0/set-expiry-sooner?"+v.Encode(), 200, nil) + return err +} + // WatchIPNBus subscribes to the IPN notification bus. It returns a watcher // once the bus is connected successfully. // diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 4e22a0688..f6e91b8f4 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -166,6 +166,16 @@ var debugCmd = &ffcli.Command{ return fs })(), }, + { + Name: "set-expire", + Exec: runSetExpire, + ShortHelp: "manipulate node key expiry for testing", + FlagSet: (func() *flag.FlagSet { + fs := newFlagSet("set-expire") + fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now") + return fs + })(), + }, { Name: "dev-store-set", Exec: runDevStoreSet, @@ -714,3 +724,14 @@ func runDebugDERP(ctx context.Context, args []string) error { fmt.Printf("%s\n", must.Get(json.MarshalIndent(st, "", " "))) return nil } + +var setExpireArgs struct { + in time.Duration +} + +func runSetExpire(ctx context.Context, args []string) error { + if len(args) != 0 || setExpireArgs.in == 0 { + return errors.New("usage --in=") + } + return localClient.DebugSetExpireIn(ctx, setExpireArgs.in) +} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index fabc8952c..c46e81dc6 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -1065,6 +1065,10 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) { // 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 !h.PermitWrite { + http.Error(w, "access denied", http.StatusForbidden) + return + } if r.Method != "POST" { http.Error(w, "POST required", http.StatusMethodNotAllowed) return