From 051a0ecd627b9a7a3c283736d5b3ffa85b5ccc1e Mon Sep 17 00:00:00 2001 From: kari-ts <135075563+kari-ts@users.noreply.github.com> Date: Thu, 22 Feb 2024 09:24:55 -0800 Subject: [PATCH] cmd/tailscale/main: use localapi for edit prefs -Use PrefsView instead of Prefs -Replace SetPrefs calls with localapi using the prefs endpoint -Follow-up will be to check if an exit node is being run from this node and stop before editing prefs to use an exit node; and to check if an exit node is being used and stop before editing prefs to be an exit node (right now there is no check and prefs are just being overriden directly) Updates tailscale/tailscale#10992 Signed-off-by: kari-ts --- cmd/tailscale/main.go | 138 ++++++++++++++++++++++++++++-------------- cmd/tailscale/ui.go | 12 ++-- go.mod | 4 +- 3 files changed, 98 insertions(+), 56 deletions(-) diff --git a/cmd/tailscale/main.go b/cmd/tailscale/main.go index 61769a9..115c3c2 100644 --- a/cmd/tailscale/main.go +++ b/cmd/tailscale/main.go @@ -5,11 +5,13 @@ package main import ( + "bytes" "cmp" "context" "crypto/rand" "crypto/sha1" "encoding/hex" + "encoding/json" "errors" "fmt" "io" @@ -66,15 +68,17 @@ type App struct { // netStates receives the most recent network state. netStates chan BackendState // prefs receives new preferences from the backend. - prefs chan *ipn.Prefs + prefs chan ipn.PrefsView // browseURLs receives URLs when the backend wants to browse. browseURLs chan string // targetsLoaded receives lists of file targets. 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 receives the bug report from the backend's localapi call. bugReport chan string + // editFns receives instructions for which preferences to edit and enforces the ordering of these edits. + editFns chan func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) } var ( @@ -153,7 +157,7 @@ type Peer struct { } type BackendState struct { - Prefs *ipn.Prefs + Prefs ipn.PrefsView State ipn.State NetworkMap *netmap.NetworkMap LostInternet bool @@ -227,10 +231,11 @@ func main() { appCtx: jni.Object(app.AppContext()), netStates: make(chan BackendState, 1), browseURLs: make(chan string, 1), - prefs: make(chan *ipn.Prefs, 1), + prefs: make(chan ipn.PrefsView, 1), targetsLoaded: make(chan FileTargets, 1), invalidates: make(chan struct{}, 1), bugReport: make(chan string, 1), + editFns: make(chan func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs), 1), } err := jni.Do(a.jvm, func(env *jni.Env) error { loader := jni.ClassLoaderFor(env, a.appCtx) @@ -370,16 +375,19 @@ func (a *App) runBackend(ctx context.Context) error { case n := <-notifications: exitWasOnline := state.ExitStatus == ExitOnline if p := n.Prefs; p != nil && n.Prefs.Valid() { - state.Prefs = p.AsStruct() + // make a copy of prefs and use the readonly view + state.Prefs = p.AsStruct().View() state.updateExitNodes() - a.setPrefs(state.Prefs) + a.prefs <- state.Prefs } - first := state.Prefs == nil + first := !state.Prefs.Valid() if first { - state.Prefs = ipn.NewPrefs() - state.Prefs.Hostname = a.hostname() - go b.backend.SetPrefs(state.Prefs) - a.setPrefs(state.Prefs) + newPrefs := ipn.NewPrefs() + newPrefs.Hostname = a.hostname() + go b.backend.SetPrefs(newPrefs) + state.Prefs = newPrefs.View() + + go a.editPrefs(ctx, state.Prefs) } if s := n.State; s != nil { oldState := state.State @@ -451,11 +459,21 @@ func (a *App) runBackend(ctx context.Context) error { case OAuth2Event: go b.backend.Login(e.Token) case BeExitNodeEvent: - state.Prefs.SetAdvertiseExitNode(bool(e)) - go b.backend.SetPrefs(state.Prefs) + a.editFns <- func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) { + routes := currPrefs.AdvertiseRoutes() + routes.AppendTo([]netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("::/0"), + }) + updatedPrefs.AdvertiseRoutes = routes.AsSlice() + updatedPrefs.SetAdvertiseExitNode(bool(e)) + updatedPrefs.AdvertiseRoutesSet = true + } case ExitAllowLANEvent: - state.Prefs.ExitNodeAllowLANAccess = bool(e) - go b.backend.SetPrefs(state.Prefs) + a.editFns <- func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) { + updatedPrefs.ExitNodeAllowLANAccess = bool(e) + updatedPrefs.ExitNodeAllowLANAccessSet = true + } case WebAuthEvent: log.Printf("KARI WEBAUTHEVENT") if !signingIn { @@ -463,17 +481,10 @@ func (a *App) runBackend(ctx context.Context) error { signingIn = true } case SetLoginServerEvent: - state.Prefs.ControlURL = e.URL - b.backend.SetPrefs(state.Prefs) - // Need to restart to force the login URL to be regenerated - // with the new control URL. Start from a goroutine to avoid - // deadlock. - go func() { - err := b.backend.Start(ipn.Options{}) - if err != nil { - fatalErr(err) - } - }() + a.editFns <- func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) { + updatedPrefs.ControlURL = e.URL + updatedPrefs.ControlURLSet = true + } case LogoutEvent: go func() { ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) @@ -481,11 +492,15 @@ func (a *App) runBackend(ctx context.Context) error { b.backend.Logout(ctx) }() case ConnectEvent: - state.Prefs.WantRunning = e.Enable - go b.backend.SetPrefs(state.Prefs) + a.editFns <- func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) { + updatedPrefs.WantRunning = e.Enable + updatedPrefs.WantRunningSet = true + } case RouteAllEvent: - state.Prefs.ExitNodeID = e.ID - go b.backend.SetPrefs(state.Prefs) + a.editFns <- func(currPrefs ipn.PrefsView, updatedPrefs *ipn.MaskedPrefs) { + updatedPrefs.ExitNodeID = e.ID + updatedPrefs.ExitNodeIDSet = true + } state.updateExitNodes() a.notify(state) if service != 0 { @@ -570,6 +585,46 @@ func (a *App) runBackend(ctx context.Context) error { } } +// editPrefs is called when prefs is created for the first time. It pulls +// from the edit function channel, and makes calls to localapi to edit the +// +// preferences. This ensures that edits will be made in the correct order. +func (a *App) editPrefs(ctx context.Context, prefs ipn.PrefsView) { + for { + select { + case fn := <-a.editFns: + mp := new(ipn.MaskedPrefs) + fn(prefs, mp) + + jsonData, err := json.Marshal(mp) + if err != nil { + log.Printf("error marshaling MaskedPrefs %v", err) + continue + } + + body := bytes.NewReader(jsonData) + r, err := a.localAPIClient.Call(ctx, "PATCH", "prefs", body) + if err != nil { + log.Printf("localapiclient error %v", err) + continue + } + + data, err := io.ReadAll(r.Body()) + r.Body().Close() + if err != nil { + log.Printf("error reading localapi response %v", err) + } + var newPrefs ipn.PrefsView + err = json.Unmarshal(data, &newPrefs) + if err != nil { + log.Printf("error unmarshaling localapi response %v", err) + } + prefs = newPrefs + default: + } + } +} + 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() @@ -704,8 +759,8 @@ func (a *App) isChromeOS() bool { func (s *BackendState) updateExitNodes() { s.ExitStatus = ExitNone var exitID tailcfg.StableNodeID - if p := s.Prefs; p != nil { - exitID = p.ExitNodeID + if p := s.Prefs; p.Valid() { + exitID = p.ExitNodeID() if exitID != "" { s.ExitStatus = ExitOffline } @@ -978,18 +1033,6 @@ func (a *App) notify(state BackendState) { } } -func (a *App) setPrefs(prefs *ipn.Prefs) { - wantRunning := jni.Bool(prefs.WantRunning) - if err := a.callVoidMethod(a.appCtx, "setTileStatus", "(Z)V", jni.Value(wantRunning)); err != nil { - fatalErr(err) - } - select { - case <-a.prefs: - default: - } - a.prefs <- prefs -} - func (a *App) setURL(url string) { select { case <-a.browseURLs: @@ -1045,9 +1088,10 @@ func (a *App) runUI() error { } } case p := <-a.prefs: - ui.enabled.Value = p.WantRunning + ui.enabled.Value = p.WantRunning() ui.runningExit = p.AdvertisesExitNode() - ui.exitLAN.Value = p.ExitNodeAllowLANAccess + ui.exitID = p.ExitNodeID() + ui.exitLAN.Value = p.ExitNodeAllowLANAccess() w.Invalidate() case url := <-a.browseURLs: ui.signinType = noSignin @@ -1313,7 +1357,7 @@ func (a *App) processUIEvents(w *app.Window, events []UIEvent, act jni.Object, s a.signOut() requestBackend(e) case ConnectEvent: - if srv, _ := a.store.ReadString(customLoginServerPrefKey, ""); srv != state.backend.Prefs.ControlURL { + if srv, _ := a.store.ReadString(customLoginServerPrefKey, ""); srv != state.backend.Prefs.ControlURL() { requestBackend(SetLoginServerEvent{URL: srv}) // wait a moment for the backend to restart <-time.After(200 * time.Millisecond) diff --git a/cmd/tailscale/ui.go b/cmd/tailscale/ui.go index e6ecbf5..22b7631 100644 --- a/cmd/tailscale/ui.go +++ b/cmd/tailscale/ui.go @@ -82,6 +82,8 @@ type UI struct { runningExit bool // are we an exit node now? + exitID tailcfg.StableNodeID + qr struct { show bool op paint.ImageOp @@ -360,7 +362,6 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat localName, localAddr string expiry time.Time userID tailcfg.UserID - exitID tailcfg.StableNodeID ) if netmap != nil { userID = netmap.User() @@ -370,16 +371,13 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat localAddr = addrs.At(0).Addr().String() } } - if p := state.backend.Prefs; p != nil { - exitID = p.ExitNodeID - } if d := &ui.exitDialog; d.show { - if newID := tailcfg.StableNodeID(d.exits.Value); newID != exitID { + if newID := tailcfg.StableNodeID(d.exits.Value); newID != ui.exitID { d.show = false events = append(events, RouteAllEvent{newID}) } } else { - d.exits.Value = string(exitID) + d.exits.Value = string(ui.exitID) } if ui.exitLAN.Changed() { events = append(events, ExitAllowLANEvent(ui.exitLAN.Value)) @@ -583,7 +581,7 @@ func (ui *UI) layout(gtx layout.Context, sysIns system.Insets, state *clientStat // 3-dots menu. if ui.menu.show { - ui.layoutMenu(gtx, sysIns, expiry, exitID != "" || len(state.backend.Exits) > 0, needsLogin) + ui.layoutMenu(gtx, sysIns, expiry, ui.exitID != "" || len(state.backend.Exits) > 0, needsLogin) } if ui.qr.show { diff --git a/go.mod b/go.mod index 959f759..6ccc483 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tailscale/tailscale-android -go 1.22 +go 1.22.0 require ( eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 @@ -98,4 +98,4 @@ require ( gvisor.dev/gvisor v0.0.0-20240119233241-c9c1d4f9b186 // indirect inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect nhooyr.io/websocket v1.8.10 // indirect -) \ No newline at end of file +)