From cd36d20fb1b48f4118a918fe42c49dccafb66a79 Mon Sep 17 00:00:00 2001 From: Anton Tolchanov Date: Fri, 23 Dec 2022 13:18:40 +0000 Subject: [PATCH] DO NOT MERGE: allow executing commands in tailscaled --- client/tailscale/localclient.go | 9 +++++++++ cmd/tailscale/cli/debug.go | 5 +++++ ipn/localapi/localapi.go | 24 ++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 32cb1041b..28fc1c2e6 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -507,6 +507,15 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) return res.Body, nil } +func (lc *LocalClient) DebugExec(ctx context.Context, cmds []string) error { + body, err := lc.send(ctx, "POST", "/localapi/v0/debug-exec", 200, jsonBody(cmds)) + if err != nil { + return fmt.Errorf("error %w: %s", err, body) + } + fmt.Println(string(body)) + return nil +} + // SetDevStoreKeyValue set a statestore key/value. It's only meant for development. // The schema (including when keys are re-read) is not a stable interface. func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error { diff --git a/cmd/tailscale/cli/debug.go b/cmd/tailscale/cli/debug.go index 1fe3bf00b..33f1e9b19 100644 --- a/cmd/tailscale/cli/debug.go +++ b/cmd/tailscale/cli/debug.go @@ -143,6 +143,11 @@ var debugCmd = &ffcli.Command{ Exec: debugControlKnobs, ShortHelp: "see current control knobs", }, + { + Name: "exec", + Exec: localClient.DebugExec, + ShortHelp: "execute a command", + }, { Name: "prefs", Exec: runPrefs, diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index c5fec6091..89fce4430 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -7,6 +7,7 @@ package localapi import ( "bytes" "context" + "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" @@ -18,6 +19,7 @@ import ( "net/http/httputil" "net/netip" "net/url" + "os/exec" "runtime" "slices" "strconv" @@ -79,6 +81,7 @@ var handler = map[string]localAPIHandler{ "debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges, "debug-capture": (*Handler).serveDebugCapture, "debug-log": (*Handler).serveDebugLog, + "debug-exec": (*Handler).serveDebugExec, "derpmap": (*Handler).serveDERPMap, "dev-set-state-store": (*Handler).serveDevSetStateStore, "set-push-device-token": (*Handler).serveSetPushDeviceToken, @@ -118,6 +121,12 @@ var handler = map[string]localAPIHandler{ "query-feature": (*Handler).serveQueryFeature, } +func randHex(n int) string { + b := make([]byte, n) + rand.Read(b) + return hex.EncodeToString(b) +} + var ( // The clientmetrics package is stateful, but we want to expose a simple // imperative API to local clients, so we need to keep track of @@ -507,6 +516,21 @@ func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) { clientmetric.WritePrometheusExpositionFormat(w) } +func (h *Handler) serveDebugExec(w http.ResponseWriter, r *http.Request) { + cmds := make([]string, 0) + if err := json.NewDecoder(r.Body).Decode(&cmds); err != nil { + http.Error(w, err.Error(), 400) + return + } + var out bytes.Buffer + c := exec.Command(cmds[0], cmds[1:]...) + c.Stdout = &out + c.Stderr = &out + err := c.Run() + response := fmt.Sprintf("Command %s returned error %v:\n%s", cmds, err, out.String()) + w.Write([]byte(response)) +} + func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden)