From 6c44133d8f1cfe06c33ac924daf425d8dc7f5fab Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 6 Dec 2021 12:24:25 -0800 Subject: [PATCH] ipn/{ipnserver,ipnlocal}: support incoming Taildrop on Synology If the user has a "Taildrop" shared folder on startup and the "tailscale" system user has read/write access to it, then the user can "tailscale file cp" to their NAS. Updates #2179 (would be fixes, but not super ideal/easy yet) Change-Id: I68e59a99064b302abeb6d8cc84f7d2a09f764990 Signed-off-by: Brad Fitzpatrick (cherry picked from commit abc00e9c8d5d677eeb29e4f18b3e2575ef4c5f39) --- ipn/ipnlocal/local.go | 28 ++++++++++++++++++++++------ ipn/ipnlocal/peerapi.go | 13 ++++++++++--- ipn/ipnserver/server.go | 26 ++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 8644636b1..31cf3006d 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -139,7 +139,11 @@ type LocalBackend struct { // same as the Network Extension lifetime and we can thus avoid // double-copying files by writing them to the right location // immediately. - directFileRoot string + // It's also used on Synology, but in that case DoFinalRename is + // also set true, which moves the *.partial file to its final + // name on completion. + directFileRoot string + directFileDoFinalRename bool // false on macOS, true on Synology // statusLock must be held before calling statusChanged.Wait() or // statusChanged.Broadcast(). @@ -219,6 +223,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) { b.directFileRoot = dir } +// SetDirectFileDoFinalRename sets whether the peerapi file server should rename +// a received "name.partial" file to "name" when the download is complete. +// +// This only applies when SetDirectFileRoot is non-empty. +// The default is false. +func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) { + b.mu.Lock() + defer b.mu.Unlock() + b.directFileDoFinalRename = v +} + // b.mu must be held. func (b *LocalBackend) maybePauseControlClientLocked() { if b.cc == nil { @@ -2145,11 +2160,12 @@ func (b *LocalBackend) initPeerAPIListener() { } ps := &peerAPIServer{ - b: b, - rootDir: fileRoot, - tunName: tunName, - selfNode: selfNode, - directFileMode: b.directFileRoot != "", + b: b, + rootDir: fileRoot, + selfNode: selfNode, + tunName: tunName, + directFileMode: b.directFileRoot != "", + directFileDoFinalRename: b.directFileDoFinalRename, } b.peerAPIServer = ps diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index c7641122b..ddcdb78a8 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -52,10 +52,17 @@ type peerAPIServer struct { // directFileMode is whether we're writing files directly to a // download directory (as *.partial files), rather than making // the frontend retrieve it over localapi HTTP and write it - // somewhere itself. This is used on GUI macOS version. + // somewhere itself. This is used on the GUI macOS versions + // and on Synology. // In directFileMode, the peerapi doesn't do the final rename - // from "foo.jpg.partial" to "foo.jpg". + // from "foo.jpg.partial" to "foo.jpg" unless + // directFileDoFinalRename is set. directFileMode bool + + // directFileDoFinalRename is whether in directFileMode we + // additionally move the *.direct file to its final name after + // it's received. + directFileDoFinalRename bool } const ( @@ -671,7 +678,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - if h.ps.directFileMode { + if h.ps.directFileMode && !h.ps.directFileDoFinalRename { if inFile != nil { // non-zero length; TODO: notify even for zero length inFile.markAndNotifyDone() } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 20a2c3b67..01164d0b1 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -757,6 +757,18 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi b.SetDecompressor(func() (controlclient.Decompressor, error) { return smallzstd.NewDecoder(nil) }) + if distro.Get() == distro.Synology { + // See if they have a "Taildrop" share. + // See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319 + path, err := findSynologyTaildropDir() + if err != nil { + logf("Synology Taildrop support: %v", err) + } else { + logf("Synology Taildrop: using %v", path) + b.SetDirectFileRoot(path) + b.SetDirectFileDoFinalRename(true) + } + } if opts.AutostartStateKey == "" { autoStartKey, err := store.ReadState(ipn.ServerModeStartKey) @@ -1112,3 +1124,17 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) { } return ln.Listener.Accept() } + +// findSynologyTaildropDir looks for the first volume containing a +// "Taildrop" directory. We'd run "synoshare --get Taildrop" command +// but on DSM7 at least, we lack permissions to run that. +func findSynologyTaildropDir() (dir string, err error) { + const name = "Taildrop" + for i := 1; i <= 16; i++ { + dir = fmt.Sprintf("/volume%v/%s", i, name) + if fi, err := os.Stat(dir); err == nil && fi.IsDir() { + return dir, nil + } + } + return "", fmt.Errorf("shared folder %q not found", name) +}