From b8ede586039bacfd889b00ac997183d0c59f62d1 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Sun, 30 Apr 2023 12:59:01 -0400 Subject: [PATCH 1/3] internal/tsutil: begin experimenting with `IPNBusWatcher` --- internal/tsutil/client.go | 9 +++- internal/tsutil/poller.go | 102 ++++++++++++++++++-------------------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/internal/tsutil/client.go b/internal/tsutil/client.go index dd596cd..0857f59 100644 --- a/internal/tsutil/client.go +++ b/internal/tsutil/client.go @@ -51,6 +51,11 @@ func (c *Client) run(ctx context.Context, args ...string) (string, error) { return out.String(), err } +// Watch returns an IPNBusWatcher to get notifications from the Tailscale daemon. +func (c *Client) Watch(ctx context.Context) (*tailscale.IPNBusWatcher, error) { + return localClient.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyInitialPrefs) +} + // Status returns the status of the connection to the Tailscale // network. If the network is not currently connected, it returns // nil, nil. @@ -81,7 +86,7 @@ func (c *Client) Stop(ctx context.Context) error { // ExitNode uses the specified peer as an exit node, or unsets // an existing exit node if peer is nil. -func (c *Client) ExitNode(ctx context.Context, peer *ipnstate.PeerStatus) error { +func (c *Client) ExitNode(ctx context.Context, peer *tailcfg.Node) error { if peer == nil { var prefs ipn.Prefs prefs.ClearExitNode() @@ -102,7 +107,7 @@ func (c *Client) ExitNode(ctx context.Context, peer *ipnstate.PeerStatus) error } var prefs ipn.Prefs - prefs.SetExitNodeIP(peer.TailscaleIPs[0].String(), status) + prefs.SetExitNodeIP(peer.Addresses[0].String(), status) _, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{ Prefs: prefs, ExitNodeIDSet: true, diff --git a/internal/tsutil/poller.go b/internal/tsutil/poller.go index 0c82485..214ec8c 100644 --- a/internal/tsutil/poller.go +++ b/internal/tsutil/poller.go @@ -2,13 +2,12 @@ package tsutil import ( "context" + "fmt" "sync" - "time" "deedles.dev/mk" - "golang.org/x/exp/slog" "tailscale.com/ipn" - "tailscale.com/ipn/ipnstate" + "tailscale.com/types/netmap" ) // A Poller gets the latest Tailscale status at regular intervals or @@ -29,13 +28,11 @@ type Poller struct { New func(Status) once sync.Once - poll chan struct{} get chan Status } func (p *Poller) init() { p.once.Do(func() { - mk.Chan(&p.poll, 0) mk.Chan(&p.get, 0) }) } @@ -52,66 +49,52 @@ func (p *Poller) client() *Client { // // The behavior of two calls to Run running concurrently is undefined. // Don't do it. -func (p *Poller) Run(ctx context.Context) { +func (p *Poller) Run(ctx context.Context) error { p.init() - const ticklen = 5 * time.Second - check := time.NewTicker(ticklen) - defer check.Stop() + w, err := p.TS.Watch(ctx) + if err != nil { + return fmt.Errorf("watch: %w", err) + } + defer w.Close() - for { - status, err := p.client().Status(ctx) - if err != nil { - if ctx.Err() != nil { + ctx, cancel := context.WithCancelCause(ctx) + defer cancel(nil) + + status := make(chan *ipn.Notify) + go func() { + for { + n, err := w.Next() + if err != nil { + cancel(fmt.Errorf("next notification: %w", err)) return } - slog.Error("get Tailscale status", "err", err) - continue - } - prefs, err := p.client().Prefs(ctx) - if err != nil { - if ctx.Err() != nil { + select { + case <-ctx.Done(): return + case status <- &n: } - slog.Error("get Tailscale prefs", "err", err) - continue } + }() - s := Status{Status: status, Prefs: prefs} - if p.New != nil { - // TODO: Only call this if the status changed from the previous - // poll? Is that remotely feasible? - p.New(s) - } - - send: + var latest Status + var get chan Status + for { select { case <-ctx.Done(): - return - case <-check.C: - case <-p.poll: - check.Reset(ticklen) - case p.get <- s: - goto send // I've never used a goto before. + return context.Cause(ctx) + case s := <-status: + latest.update(s) + if p.New != nil { + p.New(latest) + } + get = p.get + case get <- latest: } } } -// Poll returns a channel that, when sent to, causes a new status to -// be fetched from Tailscale. A send to the channel does not resolve -// until the poller begins to fetch the status, meaning that a send to -// Poll followed immediately by a receive from Get will always result -// in the new Status. -// -// Do not close the returned channel. Doing so will result in -// undefined behavior. -func (p *Poller) Poll() chan<- struct{} { - p.init() - - return p.poll -} - // Get returns a channel that will yield the latest Status fetched. If // a new Status is in the process of being fetched, it will wait for // that to finish and then yield that. @@ -124,16 +107,29 @@ func (p *Poller) Get() <-chan Status { // Status is a type that wraps various status-related types that // Tailscale provides. type Status struct { - Status *ipnstate.Status - Prefs *ipn.Prefs + State ipn.State + Prefs ipn.PrefsView + NetMap netmap.NetworkMap +} + +func (s *Status) update(n *ipn.Notify) { + if n.State != nil { + s.State = *n.State + } + if (n.Prefs != nil) && n.Prefs.Valid() { + s.Prefs = *n.Prefs + } + if n.NetMap != nil { + s.NetMap = *n.NetMap + } } // Online returns true if s indicates that the local node is online // and connected to the tailnet. func (s Status) Online() bool { - return (s.Status != nil) && (s.Status.BackendState == ipn.Running.String()) + return s.State == ipn.Running } func (s Status) NeedsAuth() bool { - return (s.Status != nil) && (s.Status.BackendState == ipn.NeedsLogin.String()) + return s.State == ipn.NeedsLogin } From bde2669abebbee9c216055ddadfa904b7edfc0c6 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Sun, 30 Apr 2023 12:59:29 -0400 Subject: [PATCH 2/3] cmd/trayscale: partially migrate to new API and find some problems --- cmd/trayscale/app.go | 25 ++++++++----------------- cmd/trayscale/trayscale.go | 12 ++++++++---- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/cmd/trayscale/app.go b/cmd/trayscale/app.go index 9d06e66..5512bce 100644 --- a/cmd/trayscale/app.go +++ b/cmd/trayscale/app.go @@ -21,7 +21,7 @@ import ( "golang.org/x/exp/maps" "golang.org/x/exp/slices" "golang.org/x/exp/slog" - "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" "tailscale.com/types/key" ) @@ -79,7 +79,7 @@ func (a *App) showAbout() { a.app.AddWindow(&dialog.Window.Window) } -func (a *App) updatePeerPage(page *peerPage, peer *ipnstate.PeerStatus, status tsutil.Status) { +func (a *App) updatePeerPage(page *peerPage, peer *tailcfg.Node, status tsutil.Status) { page.page.SetIconName(peerIcon(peer)) page.page.SetTitle(peerName(status, peer, page.self)) @@ -93,15 +93,15 @@ func (a *App) updatePeerPage(page *peerPage, peer *ipnstate.PeerStatus, status t if page.self { page.container.AdvertiseExitNodeSwitch.SetState(status.Prefs.AdvertisesExitNode()) page.container.AdvertiseExitNodeSwitch.SetActive(status.Prefs.AdvertisesExitNode()) - page.container.AllowLANAccessSwitch.SetState(status.Prefs.ExitNodeAllowLANAccess) - page.container.AllowLANAccessSwitch.SetActive(status.Prefs.ExitNodeAllowLANAccess) + page.container.AllowLANAccessSwitch.SetState(status.Prefs.ExitNodeAllowLANAccess()) + page.container.AllowLANAccessSwitch.SetActive(status.Prefs.ExitNodeAllowLANAccess()) } page.container.AdvertiseRouteButton.SetVisible(page.self) switch { case page.self: - page.routes = status.Prefs.AdvertiseRoutes + page.routes = status.Prefs.AdvertiseRoutes() case peer.PrimaryRoutes != nil: page.routes = peer.PrimaryRoutes.AsSlice() } @@ -164,7 +164,7 @@ func (a *App) updatePeers(status tsutil.Status) { w := a.win.PeersStack - var peerMap map[key.NodePublic]*ipnstate.PeerStatus + var peerMap map[key.NodePublic]*tailcfg.Node var peers []key.NodePublic if status.Online() { @@ -304,7 +304,6 @@ func (a *App) startTS(ctx context.Context) error { if err != nil { return err } - a.poller.Poll() <- struct{}{} return nil } @@ -313,7 +312,6 @@ func (a *App) stopTS(ctx context.Context) error { if err != nil { return err } - a.poller.Poll() <- struct{}{} return nil } @@ -382,7 +380,6 @@ func (a *App) onAppActivate(ctx context.Context) { a.win = nil return false }) - a.poller.Poll() <- struct{}{} a.win.Show() } @@ -496,7 +493,7 @@ func (row *routeRow) Widget() gtk.Widgetter { return row.w } -func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peerPage { +func (a *App) newPeerPage(status tsutil.Status, peer *tailcfg.Node) *peerPage { page := peerPage{ container: NewPeerPage(), } @@ -551,7 +548,6 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer slog.Error("advertise routes", "err", err) return } - a.poller.Poll() <- struct{}{} }) return &row @@ -570,7 +566,7 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer } } - var node *ipnstate.PeerStatus + var node *tailcfg.Node if s { node = peer } @@ -580,7 +576,6 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer page.container.ExitNodeSwitch.SetActive(!s) return true } - a.poller.Poll() <- struct{}{} return true }) @@ -603,7 +598,6 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer page.container.AdvertiseExitNodeSwitch.SetActive(!s) return true } - a.poller.Poll() <- struct{}{} return true }) @@ -618,7 +612,6 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer page.container.AllowLANAccessSwitch.SetActive(!s) return true } - a.poller.Poll() <- struct{}{} return true }) @@ -644,8 +637,6 @@ func (a *App) newPeerPage(status tsutil.Status, peer *ipnstate.PeerStatus) *peer slog.Error("advertise routes", "err", err) return } - - a.poller.Poll() <- struct{}{} }) }) diff --git a/cmd/trayscale/trayscale.go b/cmd/trayscale/trayscale.go index 8f697a6..dd3035d 100644 --- a/cmd/trayscale/trayscale.go +++ b/cmd/trayscale/trayscale.go @@ -11,7 +11,7 @@ import ( "deedles.dev/trayscale" "deedles.dev/trayscale/internal/tsutil" - "tailscale.com/ipn/ipnstate" + "tailscale.com/tailcfg" "tailscale.com/types/opt" ) @@ -69,9 +69,13 @@ func readAssetString(file string) string { return str.String() } -func peerName(status tsutil.Status, peer *ipnstate.PeerStatus, self bool) string { +func peerName(status tsutil.Status, peer *tailcfg.Node, self bool) string { + if peer.ComputedName == "" { + peer.InitDisplayNames("") + } + const maxNameLength = 30 - name := tsutil.DNSOrQuoteHostname(status.Status, peer) + name := peer.DisplayName(true) if len(name) > maxNameLength { name = name[:maxNameLength-3] + "..." } @@ -88,7 +92,7 @@ func peerName(status tsutil.Status, peer *ipnstate.PeerStatus, self bool) string return name } -func peerIcon(peer *ipnstate.PeerStatus) string { +func peerIcon(peer *tailcfg.Node) string { if peer.ExitNode { return "network-workgroup-symbolic" } From 29d41e438b59d5da189e84e41877b852ba845e87 Mon Sep 17 00:00:00 2001 From: DeedleFake Date: Sun, 30 Apr 2023 21:03:58 -0400 Subject: [PATCH 3/3] cmd/trayscale: more migration --- cmd/trayscale/app.go | 41 +++++++++++++++++++------------------- cmd/trayscale/trayscale.go | 15 ++++++++++++++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/cmd/trayscale/app.go b/cmd/trayscale/app.go index 5512bce..2ed70b0 100644 --- a/cmd/trayscale/app.go +++ b/cmd/trayscale/app.go @@ -4,7 +4,6 @@ import ( "context" "net/netip" "os" - "strconv" "time" "deedles.dev/mk" @@ -83,11 +82,11 @@ func (a *App) updatePeerPage(page *peerPage, peer *tailcfg.Node, status tsutil.S page.page.SetIconName(peerIcon(peer)) page.page.SetTitle(peerName(status, peer, page.self)) - page.container.SetTitle(peer.HostName) - page.container.SetDescription(peer.DNSName) + page.container.SetTitle(peer.Hostinfo.Hostname()) + page.container.SetDescription(peer.Name) - slices.SortFunc(peer.TailscaleIPs, netip.Addr.Less) - page.addrRows.Update(peer.TailscaleIPs) + slices.SortFunc(peer.Addresses, netipPrefixLess) + page.addrRows.Update(peer.Addresses) page.container.OptionsGroup.SetVisible(page.self) if page.self { @@ -101,12 +100,12 @@ func (a *App) updatePeerPage(page *peerPage, peer *tailcfg.Node, status tsutil.S switch { case page.self: - page.routes = status.Prefs.AdvertiseRoutes() + page.routes = status.Prefs.AdvertiseRoutes().AppendTo(page.routes[:0]) case peer.PrimaryRoutes != nil: - page.routes = peer.PrimaryRoutes.AsSlice() + page.routes = peer.PrimaryRoutes } page.routes = xslices.Filter(page.routes, func(p netip.Prefix) bool { return p.Bits() != 0 }) - slices.SortFunc(page.routes, func(p1, p2 netip.Prefix) bool { return p1.Addr().Less(p2.Addr()) || p1.Bits() < p2.Bits() }) + slices.SortFunc(page.routes, netipPrefixLess) if len(page.routes) == 0 { page.routes = append(page.routes, netip.Prefix{}) } @@ -122,17 +121,17 @@ func (a *App) updatePeerPage(page *peerPage, peer *tailcfg.Node, status tsutil.S page.container.NetCheckGroup.SetVisible(page.self) page.container.MiscGroup.SetVisible(!page.self) - page.container.ExitNodeRow.SetVisible(peer.ExitNodeOption) - page.container.ExitNodeSwitch.SetState(peer.ExitNode) - page.container.ExitNodeSwitch.SetActive(peer.ExitNode) - page.container.RxBytes.SetText(strconv.FormatInt(peer.RxBytes, 10)) - page.container.TxBytes.SetText(strconv.FormatInt(peer.TxBytes, 10)) + //page.container.ExitNodeRow.SetVisible(peer.ExitNodeOption) + //page.container.ExitNodeSwitch.SetState(peer.ExitNode) + //page.container.ExitNodeSwitch.SetActive(peer.ExitNode) + //page.container.RxBytes.SetText(strconv.FormatInt(peer.RxBytes, 10)) + //page.container.TxBytes.SetText(strconv.FormatInt(peer.TxBytes, 10)) page.container.Created.SetText(formatTime(peer.Created)) - page.container.LastSeen.SetText(formatTime(peer.LastSeen)) - page.container.LastSeenRow.SetVisible(!peer.Online) - page.container.LastWrite.SetText(formatTime(peer.LastWrite)) - page.container.LastHandshake.SetText(formatTime(peer.LastHandshake)) - page.container.Online.SetFromIconName(boolIcon(peer.Online)) + page.container.LastSeen.SetText(formatTime(*peer.LastSeen)) + page.container.LastSeenRow.SetVisible((peer.Online == nil) || !*peer.Online) + //page.container.LastWrite.SetText(formatTime(peer.LastWrite)) + //page.container.LastHandshake.SetText(formatTime(peer.LastHandshake)) + page.container.Online.SetFromIconName(optBoolIcon(pbool(peer.Online))) } func (a *App) notify(status bool) { @@ -449,7 +448,7 @@ type peerPage struct { self bool routes []netip.Prefix - addrRows rowManager[netip.Addr] + addrRows rowManager[netip.Prefix] routeRows rowManager[enum[netip.Prefix]] } @@ -499,9 +498,9 @@ func (a *App) newPeerPage(status tsutil.Status, peer *tailcfg.Node) *peerPage { } page.addrRows.Parent = page.container.IPGroup - page.addrRows.New = func(ip netip.Addr) row[netip.Addr] { + page.addrRows.New = func(ip netip.Prefix) row[netip.Prefix] { row := addrRow{ - ip: ip, + ip: ip.Addr(), w: adw.NewActionRow(), c: gtk.NewButtonFromIconName("edit-copy-symbolic"), diff --git a/cmd/trayscale/trayscale.go b/cmd/trayscale/trayscale.go index dd3035d..46891f0 100644 --- a/cmd/trayscale/trayscale.go +++ b/cmd/trayscale/trayscale.go @@ -4,8 +4,10 @@ import ( "context" _ "embed" "io" + "net/netip" "os" "os/signal" + "strconv" "strings" "time" @@ -118,6 +120,19 @@ func optBoolIcon(v opt.Bool) string { return boolIcon(b) } +func netipPrefixLess(p1, p2 netip.Prefix) bool { + return p1.Addr().Less(p2.Addr()) || p1.Bits() < p2.Bits() +} + +func pbool(v *bool) opt.Bool { + if v == nil { + return "" + } + + s := strconv.FormatBool(*v) + return opt.Bool(s) +} + func main() { pprof()