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

Updates tailscale/tailscale#10992
kari/bugreportvialocalapi
kari-ts 4 months ago
parent b96df2b830
commit 78e913086c

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

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

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

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

Loading…
Cancel
Save