From 672b9fd4bd64c7c2efca2699cee0a2b35b376550 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 30 Mar 2021 11:19:42 -0700 Subject: [PATCH] ipn{,/ipnlocal}: set new Notify.FilesWaiting when server has file(s) Signed-off-by: Brad Fitzpatrick --- ipn/backend.go | 2 ++ ipn/ipnlocal/local.go | 18 +++++++++---- ipn/ipnlocal/peerapi.go | 56 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/ipn/backend.go b/ipn/backend.go index dee548c54..60c5d4395 100644 --- a/ipn/backend.go +++ b/ipn/backend.go @@ -68,6 +68,8 @@ type Notify struct { BackendLogID *string // public logtail id used by backend PingResult *ipnstate.PingResult + FilesWaiting *empty.Message `json:",omitempty"` + // LocalTCPPort, if non-nil, informs the UI frontend which // (non-zero) localhost TCP port it's listening on. // This is currently only used by Tailscale when run in the diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index aee143ee6..4496874a1 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -107,6 +107,7 @@ type LocalBackend struct { authURL string interact bool prevIfState *interfaces.State + peerAPIServer *peerAPIServer // or nil peerAPIListeners []*peerAPIListener // statusLock must be held before calling statusChanged.Wait() or @@ -909,15 +910,20 @@ func (b *LocalBackend) readPoller() { // connected, the notification is dropped without being delivered. func (b *LocalBackend) send(n ipn.Notify) { b.mu.Lock() - notify := b.notify + notifyFunc := b.notify + apiSrv := b.peerAPIServer b.mu.Unlock() - if notify != nil { - n.Version = version.Long - notify(n) - } else { + if notifyFunc == nil { b.logf("nil notify callback; dropping %+v", n) + return + } + + n.Version = version.Long + if apiSrv != nil && apiSrv.hasFilesWaiting() { + n.FilesWaiting = &empty.Message{} } + notifyFunc(n) } // popBrowserAuthNow shuts down the data plane and sends an auth URL @@ -1489,6 +1495,7 @@ func (b *LocalBackend) initPeerAPIListener() { b.mu.Lock() defer b.mu.Unlock() + b.peerAPIServer = nil for _, pln := range b.peerAPIListeners { pln.Close() } @@ -1526,6 +1533,7 @@ func (b *LocalBackend) initPeerAPIListener() { tunName: tunName, selfNode: selfNode, } + b.peerAPIServer = ps isNetstack := wgengine.IsNetstack(b.e) for i, a := range b.netMap.Addresses { diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 0c8033cc2..d3b81e1d2 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -22,7 +22,9 @@ import ( "strings" "inet.af/netaddr" + "tailscale.com/ipn" "tailscale.com/net/interfaces" + "tailscale.com/syncs" "tailscale.com/tailcfg" "tailscale.com/wgengine" ) @@ -30,10 +32,52 @@ import ( var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error type peerAPIServer struct { - b *LocalBackend - rootDir string - tunName string - selfNode *tailcfg.Node + b *LocalBackend + rootDir string + tunName string + selfNode *tailcfg.Node + knownEmpty syncs.AtomicBool +} + +const partialSuffix = ".tspartial" + +// hasFilesWaiting reports whether any files are buffered in the +// tailscaled daemon storage. +func (s *peerAPIServer) hasFilesWaiting() bool { + if s.rootDir == "" { + return false + } + if s.knownEmpty.Get() { + // Optimization: this is usually empty, so avoid opening + // the directory and checking. We can't cache the actual + // has-files-or-not values as the macOS/iOS client might + // in the future use+delete the files directly. So only + // keep this negative cache. + return false + } + f, err := os.Open(s.rootDir) + if err != nil { + return false + } + defer f.Close() + for { + des, err := f.ReadDir(10) + for _, de := range des { + if strings.HasSuffix(de.Name(), partialSuffix) { + continue + } + if de.Type().IsRegular() { + return true + } + } + if err == io.EOF { + s.knownEmpty.Set(true) + } + if err != nil { + break + } + } + return false } func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) { @@ -221,7 +265,7 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { return } name := path.Base(r.URL.Path) - if name == "." || name == "/" { + if name == "." || name == "/" || strings.HasSuffix(name, partialSuffix) { http.Error(w, "bad filename", http.StatusForbidden) return } @@ -258,4 +302,6 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { // TODO: some real response success = true io.WriteString(w, "{}\n") + h.ps.knownEmpty.Set(false) + h.ps.b.send(ipn.Notify{}) // it will set FilesWaiting }