cmd/tailscale, ipn/localapi: add "tailscale bugreport" subcommand

Adding a subcommand which prints and logs a log marker. This should help
diagnose any issues that users face.

Fixes #1466

Signed-off-by: Maisem Ali <maisem@tailscale.com>
This commit is contained in:
Maisem Ali 2021-03-30 15:59:44 -07:00 committed by Maisem Ali
parent 09148c07ba
commit db13b2d0c8
5 changed files with 101 additions and 8 deletions

View File

@ -16,6 +16,7 @@
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/paths" "tailscale.com/paths"
@ -109,6 +110,28 @@ func Goroutines(ctx context.Context) ([]byte, error) {
return body, nil return body, nil
} }
// BugReport logs and returns a log marker that can be shared by the user with support.
func BugReport(ctx context.Context, note string) (string, error) {
u := fmt.Sprintf("http://local-tailscaled.sock/localapi/v0/bugreport?note=%s", url.QueryEscape(note))
req, err := http.NewRequestWithContext(ctx, "POST", u, nil)
if err != nil {
return "", err
}
res, err := DoLocalRequest(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return strings.TrimSpace(string(body)), nil
}
// Status returns the Tailscale daemon's status. // Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) { func Status(ctx context.Context) (*ipnstate.Status, error) {
return status(ctx, "") return status(ctx, "")

View File

@ -0,0 +1,38 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"context"
"errors"
"fmt"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
)
var bugReportCmd = &ffcli.Command{
Name: "bugreport",
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
}
func runBugReport(ctx context.Context, args []string) error {
var note string
switch len(args) {
case 0:
case 1:
note = args[0]
default:
return errors.New("unknown argumets")
}
logMarker, err := tailscale.BugReport(ctx, note)
if err != nil {
return err
}
fmt.Println(logMarker)
return nil
}

View File

@ -71,6 +71,7 @@ func Run(args []string) error {
versionCmd, versionCmd,
webCmd, webCmd,
pushCmd, pushCmd,
bugReportCmd,
}, },
FlagSet: rootfs, FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp }, Exec: func(context.Context, []string) error { return flag.ErrHelp },

View File

@ -97,8 +97,9 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections // server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend. // talking to an IPN backend.
type server struct { type server struct {
b *ipnlocal.LocalBackend b *ipnlocal.LocalBackend
logf logger.Logf logf logger.Logf
backendLogID string
// resetOnZero is whether to call bs.Reset on transition from // resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is // 1->0 connections. That is, this is whether the backend is
// being run in "client mode" that requires an active GUI // being run in "client mode" that requires an active GUI
@ -610,8 +611,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
} }
server := &server{ server := &server{
logf: logf, backendLogID: logid,
resetOnZero: !opts.SurviveDisconnects, logf: logf,
resetOnZero: !opts.SurviveDisconnects,
} }
// When the context is closed or when we return, whichever is first, close our listner // When the context is closed or when we return, whichever is first, close our listner
@ -982,7 +984,7 @@ func (psc *protoSwitchConn) Close() error {
} }
func (s *server) localhostHandler(ci connIdentity) http.Handler { func (s *server) localhostHandler(ci connIdentity) http.Handler {
lah := localapi.NewHandler(s.b) lah := localapi.NewHandler(s.b, s.logf, s.backendLogID)
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci) lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@ -6,6 +6,8 @@
package localapi package localapi
import ( import (
"crypto/rand"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -14,15 +16,23 @@
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"time"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/logger"
) )
func NewHandler(b *ipnlocal.LocalBackend) *Handler { func randHex(n int) string {
return &Handler{b: b} b := make([]byte, n)
rand.Read(b)
return hex.EncodeToString(b)
}
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID string) *Handler {
return &Handler{b: b, logf: logf, backendLogID: logID}
} }
type Handler struct { type Handler struct {
@ -37,7 +47,9 @@ type Handler struct {
// PermitWrite is whether mutating HTTP handlers are allowed. // PermitWrite is whether mutating HTTP handlers are allowed.
PermitWrite bool PermitWrite bool
b *ipnlocal.LocalBackend b *ipnlocal.LocalBackend
logf logger.Logf
backendLogID string
} }
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -69,11 +81,28 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveStatus(w, r) h.serveStatus(w, r)
case "/localapi/v0/check-ip-forwarding": case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r) h.serveCheckIPForwarding(w, r)
case "/localapi/v0/bugreport":
h.serveBugReport(w, r)
default: default:
io.WriteString(w, "tailscaled\n") io.WriteString(w, "tailscaled\n")
} }
} }
func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "bugreport access denied", http.StatusForbidden)
return
}
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
h.logf("user bugreport: %s", logMarker)
if note := r.FormValue("note"); len(note) > 0 {
h.logf("user bugreport note: %s", note)
}
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, logMarker)
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden) http.Error(w, "whois access denied", http.StatusForbidden)