cmd/tailscale: add CLI debug command to do raw LocalAPI requests

This adds a portable way to do a raw LocalAPI request without worrying
about the Unix-vs-macOS-vs-Windows ways of hitting the LocalAPI server.
(It was already possible but tedious with 'tailscale debug local-creds')

Updates tailscale/corp#24690

Change-Id: I0828ca55edaedf0565c8db192c10f24bebb95f1b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-03-20 07:39:51 -07:00 committed by Brad Fitzpatrick
parent f34e08e186
commit 984cd1cab0

View File

@ -136,6 +136,17 @@ func debugCmd() *ffcli.Command {
Exec: runLocalCreds,
ShortHelp: "Print how to access Tailscale LocalAPI",
},
{
Name: "localapi",
ShortUsage: "tailscale debug localapi [<method>] <path> [<body| \"-\">]",
Exec: runLocalAPI,
ShortHelp: "Call a LocalAPI method directly",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("localapi")
fs.BoolVar(&localAPIFlags.verbose, "v", false, "verbose; dump HTTP headers")
return fs
})(),
},
{
Name: "restun",
ShortUsage: "tailscale debug restun",
@ -451,6 +462,81 @@ func runLocalCreds(ctx context.Context, args []string) error {
return nil
}
func looksLikeHTTPMethod(s string) bool {
if len(s) > len("OPTIONS") {
return false
}
for _, r := range s {
if r < 'A' || r > 'Z' {
return false
}
}
return true
}
var localAPIFlags struct {
verbose bool
}
func runLocalAPI(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New("expected at least one argument")
}
method := "GET"
if looksLikeHTTPMethod(args[0]) {
method = args[0]
args = args[1:]
if len(args) == 0 {
return errors.New("expected at least one argument after method")
}
}
path := args[0]
if !strings.HasPrefix(path, "/localapi/") {
if !strings.Contains(path, "/") {
path = "/localapi/v0/" + path
} else {
path = "/localapi/" + path
}
}
var body io.Reader
if len(args) > 1 {
if args[1] == "-" {
fmt.Fprintf(Stderr, "# reading request body from stdin...\n")
all, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("reading Stdin: %q", err)
}
body = bytes.NewReader(all)
} else {
body = strings.NewReader(args[1])
}
}
req, err := http.NewRequest(method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return err
}
fmt.Fprintf(Stderr, "# doing request %s %s\n", method, path)
res, err := localClient.DoLocalRequest(req)
if err != nil {
return err
}
is2xx := res.StatusCode >= 200 && res.StatusCode <= 299
if localAPIFlags.verbose {
res.Write(Stdout)
} else {
if !is2xx {
fmt.Fprintf(Stderr, "# Response status %s\n", res.Status)
}
io.Copy(Stdout, res.Body)
}
if is2xx {
return nil
}
return errors.New(res.Status)
}
type localClientRoundTripper struct{}
func (localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {