From 171f6a497177bbd344fd0dd7bef5a799882d1cef Mon Sep 17 00:00:00 2001 From: kari-ts Date: Wed, 31 Jan 2024 09:45:01 -0800 Subject: [PATCH] cmd/tailscale/main: use localapi for generating bug report Fix logIDPublic and make localapiclient a package with a generic function that can be reused for all features --- cmd/localapiclient/localapiclient.go | 87 ++++++++++++++++++++++++++++ cmd/tailscale/backend.go | 4 +- cmd/tailscale/main.go | 49 ++++++++++++++-- go.mod | 2 +- go.sum | 2 +- localapiclient/go.mod | 3 + localapiclient/localapiclient.go | 81 ++++++++++++++++++++++++++ 7 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 cmd/localapiclient/localapiclient.go create mode 100644 localapiclient/go.mod create mode 100644 localapiclient/localapiclient.go diff --git a/cmd/localapiclient/localapiclient.go b/cmd/localapiclient/localapiclient.go new file mode 100644 index 0000000..6c695c5 --- /dev/null +++ b/cmd/localapiclient/localapiclient.go @@ -0,0 +1,87 @@ +package localapiclient + +import ( + "bytes" + "errors" + "fmt" + "log" + "net/http" + "time" + + "tailscale.com/ipn/localapi" +) + +// LocalAPIResponseWriter substitutes for http.ResponseWriter in order to write byte streams directly +// to a receiver function in the application. +type LocalApiResponseWriter struct { + headers http.Header + body bytes.Buffer + status int +} + +func newLocalApiResponseWriter() *LocalApiResponseWriter { + return &LocalApiResponseWriter{headers: http.Header{}, status: http.StatusOK} +} + +func (w *LocalApiResponseWriter) Header() http.Header { + return w.headers +} + +// Write writes the data to the response body, which will be sent to Java. If WriteHeader is not called +// explicitly, the first call to Write will trigger an implicit WriteHeader(http.StatusOK). +func (w *LocalApiResponseWriter) Write(data []byte) (int, error) { + if w.status == 0 { + w.WriteHeader(http.StatusOK) + } + return w.body.Write(data) +} + +func (w *LocalApiResponseWriter) WriteHeader(statusCode int) { + w.status = statusCode +} + +func (w *LocalApiResponseWriter) Body() []byte { + return w.body.Bytes() +} + +func (w *LocalApiResponseWriter) StatusCode() int { + return w.status +} + +type LocalApiClient struct { + h *localapi.Handler +} + +func NewLocalApiClient(h *localapi.Handler) LocalApiClient { + return LocalApiClient{h: h} +} + +var ErrBadHttpStatus = errors.New("bad http status for localapi response") + +func CallLocalApi(h *localapi.Handler, method string, endpoint string) (*LocalApiResponseWriter, error) { + done := make(chan *LocalApiResponseWriter, 1) + var responseError error + go func() { + req, err := http.NewRequest(method, "/localapi/v0/"+endpoint, nil) + if err != nil { + log.Printf("error creating new request for %s: %v", endpoint, err) + responseError = err + close(done) + return + } + w := newLocalApiResponseWriter() + h.ServeHTTP(w, req) + if w.StatusCode() > 300 { + log.Printf("%s bad http status: %v", endpoint, w.StatusCode()) + responseError = ErrBadHttpStatus + } + done <- w + }() + + select { + case w := <-done: + return w, responseError + case <-time.After(2 * time.Second): + return nil, fmt.Errorf("request to %s timed out", 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 dde90d9..2e3d8a1 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -33,16 +33,19 @@ import ( "gioui.org/op" "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" ) @@ -53,7 +56,7 @@ type App struct { appCtx jni.Object store *stateStore - logIDPublicAtomic atomic.Value // of string + logIDPublicAtomic atomic.Pointer[logid.PublicID] // netStates receives the most recent network state. netStates chan BackendState @@ -65,6 +68,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 ( @@ -219,6 +224,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) @@ -280,9 +286,13 @@ 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 + // Contrary to the documentation for VpnService.Builder.addDnsServer, // ChromeOS doesn't fall back to the underlying network nameservers if // we don't provide any. @@ -425,6 +435,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)) + getBugReportID(h, a.bugReport, fallbackLog) case OAuth2Event: go b.backend.Login(e.Token) case ToggleEvent: @@ -549,6 +563,15 @@ func (a *App) runBackend() error { } } +func getBugReportID(h *localapi.Handler, bugReportChan chan<- string, fallbackLog string) { + w, err := localapiclient.CallLocalApi(h, "POST", "bugreport") + if w == nil || err != nil { + bugReportChan <- fallbackLog + } else { + bugReportChan <- string(w.Body()) + } +} + func (a *App) processWaitingFiles(b *ipnlocal.LocalBackend) error { files, err := b.WaitingFiles() if err != nil { @@ -1148,10 +1171,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.mod b/go.mod index 140501c..a1d470d 100644 --- a/go.mod +++ b/go.mod @@ -99,4 +99,4 @@ require ( gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect nhooyr.io/websocket v1.8.7 // indirect -) +) \ No newline at end of file diff --git a/go.sum b/go.sum index dcdab6e..e4cb517 100644 --- a/go.sum +++ b/go.sum @@ -733,4 +733,4 @@ sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1 tailscale.com v1.1.1-0.20240118202658-8250582fe675 h1:GNiVbKAvDA4Ube6UDP+dQxT/rZ76LhHgy+PJGMJ1/oA= tailscale.com v1.1.1-0.20240118202658-8250582fe675/go.mod h1:faWR8XaXemnSKCDjHC7SAQzaagkUjA5x4jlLWiwxtuk= tailscale.com v1.58.0 h1:iLGQBaGlweNEo8kBqQHF3gcLKRR87ECfkksvFQCy2/M= -tailscale.com v1.58.0/go.mod h1:faWR8XaXemnSKCDjHC7SAQzaagkUjA5x4jlLWiwxtuk= +tailscale.com v1.58.0/go.mod h1:faWR8XaXemnSKCDjHC7SAQzaagkUjA5x4jlLWiwxtuk= \ No newline at end of file diff --git a/localapiclient/go.mod b/localapiclient/go.mod new file mode 100644 index 0000000..a90c2dc --- /dev/null +++ b/localapiclient/go.mod @@ -0,0 +1,3 @@ +module github.com/tailscale/tailscale-android/localapiclient + +go 1.21.6 diff --git a/localapiclient/localapiclient.go b/localapiclient/localapiclient.go new file mode 100644 index 0000000..fa2c24b --- /dev/null +++ b/localapiclient/localapiclient.go @@ -0,0 +1,81 @@ +package localapiclient + +import ( + "bytes" + "fmt" + "log" + "net/http" + "time" + + "tailscale.com/ipn/localapi" +) + +// LocalApiResponseWriter is our custom implementation of http.ResponseWriter +type LocalApiResponseWriter struct { + headers http.Header + body bytes.Buffer + status int +} + +func newLocalApiResponseWriter() *LocalApiResponseWriter { + return &LocalApiResponseWriter{headers: http.Header{}, status: http.StatusOK} +} + +func (w *LocalApiResponseWriter) Header() http.Header { + return w.headers +} + +// Write writes the data to the response body and will send the data to Java. +func (w *LocalApiResponseWriter) Write(data []byte) (int, error) { + if w.status == 0 { + w.WriteHeader(http.StatusOK) + } + return w.body.Write(data) +} + +func (w *LocalApiResponseWriter) WriteHeader(statusCode int) { + w.status = statusCode +} + +func (w *LocalApiResponseWriter) Body() []byte { + return w.body.Bytes() +} + +func (w *LocalApiResponseWriter) StatusCode() int { + return w.status +} + +type LocalApi interface { + GetBugReportID(result chan<- string, h *localapi.Handler, fallbackLog string) (LocalApiResponseWriter, error) +} + +type BugReportLocalApi struct{} + +func (b BugReportLocalApi) GetBugReportID(bugReportChan chan<- string, h *localapi.Handler, fallbackLog string) (*LocalApiResponseWriter, error) { + w := newLocalApiResponseWriter() + req, err := http.NewRequest("POST", "/localapi/v0/bugreport", nil) + if err != nil { + log.Printf("error creating new request for bug report: %v", err) + return w, err + } + h.ServeHTTP(w, req) + if w.StatusCode() > 300 { + err := fmt.Errorf("bug report bad http status: %v", w.StatusCode()) + log.Printf("%s", err) + bugReportChan <- fallbackLog + return w, err + } + report := string(w.Body()) + select { + case bugReportChan <- report: + err := fmt.Errorf("bug report was successfully retrieved: %s", report) + log.Printf("%s", err) + return w, err + // timeout, send fallback log + case <-time.After(2 * time.Second): + err := fmt.Errorf("bug report retrieval timed out, sending fallback log: %s", fallbackLog) + bugReportChan <- fallbackLog + log.Printf("%s", err) + return w, err + } +}