diff --git a/cmd/tailscale/backend.go b/cmd/tailscale/backend.go index 445a15f..fdd6afd 100644 --- a/cmd/tailscale/backend.go +++ b/cmd/tailscale/backend.go @@ -40,6 +40,8 @@ type backend struct { lastCfg *router.Config lastDNSCfg *dns.OSConfig + logIDPublic string + // avoidEmptyDNS controls whether to use fallback nameservers // when no nameservers are provided by Tailscale. avoidEmptyDNS bool @@ -117,7 +119,8 @@ func newBackend(dataDir string, jvm *jni.JVM, appCtx jni.Object, store *stateSto if err != nil { return nil, fmt.Errorf("runBackend: NewUserspaceEngine: %v", err) } - local, err := ipnlocal.NewLocalBackend(logf, logID.Public().String(), store, dialer, engine) + b.logIDPublic = logID.Public().String() + local, err := ipnlocal.NewLocalBackend(logf, b.logIDPublic, store, dialer, engine) if err != nil { engine.Close() return nil, fmt.Errorf("runBackend: NewLocalBackend: %v", err) diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 59066cd..4ab8520 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -6,7 +6,9 @@ package main import ( "context" + "crypto/rand" "crypto/sha1" + "encoding/hex" "errors" "fmt" "io" @@ -19,6 +21,7 @@ import ( "path/filepath" "sort" "strings" + "sync/atomic" "time" "unsafe" @@ -47,7 +50,8 @@ type App struct { // appCtx is a global reference to the com.tailscale.ipn.App instance. appCtx jni.Object - store *stateStore + store *stateStore + logIDPublicAtomic atomic.Value // of string // netStates receives the most recent network state. netStates chan BackendState @@ -180,6 +184,7 @@ type FileSendEvent struct { type ( ToggleEvent struct{} ReauthEvent struct{} + BugEvent struct{} WebAuthEvent struct{} GoogleAuthEvent struct{} LogoutEvent struct{} @@ -266,6 +271,7 @@ func (a *App) runBackend() error { if err != nil { return err } + a.logIDPublicAtomic.Store(b.logIDPublic) defer b.CloseTUNs() // Contrary to the documentation for VpnService.Builder.addDnsServer, @@ -1059,6 +1065,11 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s default: 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) case WebAuthEvent: a.store.WriteString(loginMethodPrefKey, loginMethodWeb) requestBackend(e) @@ -1341,3 +1352,9 @@ func fatalErr(err error) { // TODO: expose in UI. log.Printf("fatal error: %v", err) } + +func randHex(n int) string { + b := make([]byte, n) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go index d360bfe..d32a1f8 100644 --- a/cmd/tailscale/ui.go +++ b/cmd/tailscale/ui.go @@ -83,6 +83,7 @@ type UI struct { copy widget.Clickable reauth widget.Clickable + bug widget.Clickable exits widget.Clickable logout widget.Clickable } @@ -313,6 +314,11 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat events = append(events, ReauthEvent{}) } + if ui.menuClicked(&ui.menu.bug) { + events = append(events, BugEvent{}) + ui.showCopied(gtx, "bug report marker to clipboard") + } + if ui.menuClicked(&ui.menu.exits) || ui.openExitDialog.Clicked() { ui.exitDialog.show = true } @@ -965,12 +971,13 @@ func (ui *UI) layoutMenu(gtx layout.Context, sysIns system.Insets, expiry time.T return layout.NE.Layout(gtx, func(gtx C) D { menu := &ui.menu items := []menuItem{ - {title: "Copy My IP Address", btn: &menu.copy}, + {title: "Copy my IP address", btn: &menu.copy}, } if showExits { items = append(items, menuItem{title: "Use exit node...", btn: &menu.exits}) } items = append(items, + menuItem{title: "Bug report", btn: &menu.bug}, menuItem{title: "Reauthenticate", btn: &menu.reauth}, menuItem{title: "Log out", btn: &menu.logout}, )