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
oxtocart/bugreportvialocalapi_codereview_suggestion
kari-ts 10 months ago
parent 813ca8adea
commit 171f6a4971

@ -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)
}
}

@ -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)

@ -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:

@ -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
)
)

@ -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=

@ -0,0 +1,3 @@
module github.com/tailscale/tailscale-android/localapiclient
go 1.21.6

@ -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
}
}
Loading…
Cancel
Save