From f9310e7a1f34749e386bf734ab97baa43468ee9c Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:14:04 -0800 Subject: [PATCH] cmd/tailscale/main: use localapi for generating bug report (#155) Fix logIDPublic and make localapiclient a package with a generic function for calling localapi that can be reused for all features Updates tailscale/tailscale#10992 --- cmd/localapiclient/localapiclient.go | 102 +++++++++++++++++++++++++++ cmd/tailscale/backend.go | 4 +- cmd/tailscale/main.go | 70 +++++++++++++++--- go.sum | 4 ++ 4 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 cmd/localapiclient/localapiclient.go diff --git a/cmd/localapiclient/localapiclient.go b/cmd/localapiclient/localapiclient.go new file mode 100644 index 0000000..bfd9393 --- /dev/null +++ b/cmd/localapiclient/localapiclient.go @@ -0,0 +1,102 @@ +package localapiclient + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "sync" + + "tailscale.com/ipn/localapi" +) + +// Response represents the result of processing an http.Request. +type Response struct { + headers http.Header + status int + bodyWriter net.Conn + bodyReader net.Conn + startWritingBody chan interface{} + startWritingBodyOnce sync.Once +} + +func (r *Response) Header() http.Header { + return r.headers +} + +// Write writes the data to the response body and will send the data to Java. +func (r *Response) Write(data []byte) (int, error) { + r.Flush() + if r.status == 0 { + r.WriteHeader(http.StatusOK) + } + return r.bodyWriter.Write(data) +} + +func (r *Response) WriteHeader(statusCode int) { + r.status = statusCode +} + +func (r *Response) Body() net.Conn { + return r.bodyReader +} + +func (r *Response) StatusCode() int { + return r.status +} + +func (r *Response) Flush() { + r.startWritingBodyOnce.Do(func() { + close(r.startWritingBody) + }) +} + +type LocalAPIClient struct { + h *localapi.Handler +} + +func New(h *localapi.Handler) *LocalAPIClient { + return &LocalAPIClient{h: h} +} + +// Call calls the given endpoint on the local API using the given HTTP method +// optionally sending the given body. It returns a Response representing the +// result of the call and an error if the call could not be completed or the +// local API returned a status code in the 400 series or greater. +// Note - Response includes a response body available from the Body method, it +// is the caller's responsibility to close this. +func (cl *LocalAPIClient) Call(ctx context.Context, method, endpoint string, body io.Reader) (*Response, error) { + req, err := http.NewRequestWithContext(ctx, method, "/localapi/v0/"+endpoint, body) + if err != nil { + return nil, fmt.Errorf("error creating new request for %s: %w", endpoint, err) + } + deadline, _ := ctx.Deadline() + pipeReader, pipeWriter := net.Pipe() + pipeReader.SetDeadline(deadline) + pipeWriter.SetDeadline(deadline) + + resp := &Response{ + headers: http.Header{}, + status: http.StatusOK, + bodyReader: pipeReader, + bodyWriter: pipeWriter, + startWritingBody: make(chan interface{}), + } + + go func() { + cl.h.ServeHTTP(resp, req) + resp.Flush() + pipeWriter.Close() + }() + + select { + case <-resp.startWritingBody: + if resp.StatusCode() >= 400 { + return resp, fmt.Errorf("request failed with status code %d", resp.StatusCode()) + } + return resp, nil + case <-ctx.Done(): + return nil, fmt.Errorf("timeout for %s", endpoint) + } +} diff --git a/cmd/tailscale/backend.go b/cmd/tailscale/backend.go index d3d0336..5eae9f2 100644 --- a/cmd/tailscale/backend.go +++ b/cmd/tailscale/backend.go @@ -48,7 +48,7 @@ type backend struct { lastDNSCfg *dns.OSConfig netMon *netmon.Monitor - logIDPublic string + logIDPublic logid.PublicID logger *logtail.Logger // avoidEmptyDNS controls whether to use fallback nameservers @@ -150,7 +150,7 @@ func newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateSto return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err) } sys.Set(engine) - b.logIDPublic = logID.Public().String() + b.logIDPublic = logID.Public() ns, err := netstack.Create(logf, sys.Tun.Get(), engine, sys.MagicSock.Get(), dialer, sys.DNSManager.Get(), sys.ProxyMapper()) if err != nil { return nil, fmt.Errorf("netstack.Create: %w", err) diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 74f8d6a..5773cee 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -35,16 +35,19 @@ import ( "golang.org/x/exp/maps" "inet.af/netaddr" + "github.com/tailscale/tailscale-android/cmd/localapiclient" "github.com/tailscale/tailscale-android/jni" "tailscale.com/client/tailscale/apitype" "tailscale.com/hostinfo" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" + "tailscale.com/ipn/localapi" "tailscale.com/net/dns" "tailscale.com/net/interfaces" "tailscale.com/net/netns" "tailscale.com/paths" "tailscale.com/tailcfg" + "tailscale.com/types/logid" "tailscale.com/types/netmap" "tailscale.com/wgengine/router" ) @@ -55,7 +58,9 @@ type App struct { appCtx jni.Object store *stateStore - logIDPublicAtomic atomic.Value // of string + logIDPublicAtomic atomic.Pointer[logid.PublicID] + + localAPIClient *localapiclient.LocalAPIClient // netStates receives the most recent network state. netStates chan BackendState @@ -67,6 +72,8 @@ type App struct { targetsLoaded chan FileTargets // invalidates receives whenever the window should be refreshed. invalidates chan struct{} + // bugReport receives the bug report from the backend's localapi call + bugReport chan string } var ( @@ -223,6 +230,7 @@ func main() { prefs: make(chan *ipn.Prefs, 1), targetsLoaded: make(chan FileTargets, 1), invalidates: make(chan struct{}, 1), + bugReport: make(chan string, 1), } err := jni.Do(a.jvm, func(env *jni.Env) error { loader := jni.ClassLoaderFor(env, a.appCtx) @@ -240,7 +248,8 @@ func main() { a.store = newStateStore(a.jvm, a.appCtx) interfaces.RegisterInterfaceGetter(a.getInterfaces) go func() { - if err := a.runBackend(); err != nil { + ctx := context.Background() + if err := a.runBackend(ctx); err != nil { fatalErr(err) } }() @@ -252,7 +261,7 @@ func main() { app.Main() } -func (a *App) runBackend() error { +func (a *App) runBackend(ctx context.Context) error { appDir, err := app.DataDir() if err != nil { fatalErr(err) @@ -284,9 +293,14 @@ func (a *App) runBackend() error { if err != nil { return err } - a.logIDPublicAtomic.Store(b.logIDPublic) + a.logIDPublicAtomic.Store(&b.logIDPublic) defer b.CloseTUNs() + h := localapi.NewHandler(b.backend, log.Printf, b.sys.NetMon.Get(), *a.logIDPublicAtomic.Load()) + h.PermitRead = true + h.PermitWrite = true + a.localAPIClient = localapiclient.New(h) + // Contrary to the documentation for VpnService.Builder.addDnsServer, // ChromeOS doesn't fall back to the underlying network nameservers if // we don't provide any. @@ -429,6 +443,10 @@ func (a *App) runBackend() error { } case e := <-backendEvents: switch e := e.(type) { + case BugEvent: + backendLogIDStr := a.logIDPublicAtomic.Load().String() + fallbackLog := fmt.Sprintf("BUG-%v-%v-%v", backendLogIDStr, time.Now().UTC().Format("20060102150405Z"), randHex(8)) + a.getBugReportID(ctx, a.bugReport, fallbackLog) case OAuth2Event: go b.backend.Login(e.Token) case ToggleEvent: @@ -459,7 +477,7 @@ func (a *App) runBackend() error { }() case LogoutEvent: go func() { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() b.backend.Logout(ctx) }() @@ -553,6 +571,26 @@ func (a *App) runBackend() error { } } +func (a *App) getBugReportID(ctx context.Context, bugReportChan chan<- string, fallbackLog string) { + ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second)) + defer cancel() + r, err := a.localAPIClient.Call(ctx, "POST", "bugreport", nil) + defer r.Body().Close() + + if err != nil { + log.Printf("get bug report: %s", err) + bugReportChan <- fallbackLog + return + } + logBytes, err := io.ReadAll(r.Body()) + if err != nil { + log.Printf("read bug report: %s", err) + bugReportChan <- fallbackLog + return + } + bugReportChan <- string(logBytes) +} + func (a *App) processWaitingFiles(b *ipnlocal.LocalBackend) error { files, err := b.WaitingFiles() if err != nil { @@ -1232,10 +1270,24 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s requestBackend(WebAuthEvent{}) } case BugEvent: - backendLogID, _ := a.logIDPublicAtomic.Load().(string) - logMarker := fmt.Sprintf("BUG-%v-%v-%v", backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8)) - log.Printf("user bugreport: %s", logMarker) - w.WriteClipboard(logMarker) + // clear the channel in case there's an old bug report hanging out there + select { + case oldReport := <-a.bugReport: + log.Printf("clearing old bug report in channel: %s", oldReport) + default: + break + } + requestBackend(e) + select { + case bug := <-a.bugReport: + w.WriteClipboard(bug) + case <-time.After(2 * time.Second): + // if we don't get a bug through the channel, fall back and create bug report here + backendLogID := a.logIDPublicAtomic.Load() + logMarker := fmt.Sprintf("BUG-%v-%v-%v", backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8)) + log.Printf("bug report fallback because timed out. fallback report: %s", logMarker) + w.WriteClipboard(logMarker) + } case BeExitNodeEvent: requestBackend(e) case ExitAllowLANEvent: diff --git a/go.sum b/go.sum index fb0783e..18d947e 100644 --- a/go.sum +++ b/go.sum @@ -311,6 +311,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -410,6 +412,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 h1:G github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ= github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=