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 <bradfitz@tailscale.com>
(cherry picked from commit abc00e9c8d)
This commit is contained in:
Brad Fitzpatrick 2021-12-06 12:24:25 -08:00
parent 2d6404609d
commit 6c44133d8f
3 changed files with 58 additions and 9 deletions

View File

@ -139,7 +139,11 @@ type LocalBackend struct {
// same as the Network Extension lifetime and we can thus avoid // same as the Network Extension lifetime and we can thus avoid
// double-copying files by writing them to the right location // double-copying files by writing them to the right location
// immediately. // 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 // statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast(). // statusChanged.Broadcast().
@ -219,6 +223,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.directFileRoot = dir 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. // b.mu must be held.
func (b *LocalBackend) maybePauseControlClientLocked() { func (b *LocalBackend) maybePauseControlClientLocked() {
if b.cc == nil { if b.cc == nil {
@ -2145,11 +2160,12 @@ func (b *LocalBackend) initPeerAPIListener() {
} }
ps := &peerAPIServer{ ps := &peerAPIServer{
b: b, b: b,
rootDir: fileRoot, rootDir: fileRoot,
tunName: tunName, selfNode: selfNode,
selfNode: selfNode, tunName: tunName,
directFileMode: b.directFileRoot != "", directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
} }
b.peerAPIServer = ps b.peerAPIServer = ps

View File

@ -52,10 +52,17 @@ type peerAPIServer struct {
// directFileMode is whether we're writing files directly to a // directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making // download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it // 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 // 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 directFileMode bool
// directFileDoFinalRename is whether in directFileMode we
// additionally move the *.direct file to its final name after
// it's received.
directFileDoFinalRename bool
} }
const ( const (
@ -671,7 +678,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if h.ps.directFileMode { if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone() inFile.markAndNotifyDone()
} }

View File

@ -757,6 +757,18 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
b.SetDecompressor(func() (controlclient.Decompressor, error) { b.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil) 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 == "" { if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey) autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
@ -1112,3 +1124,17 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
} }
return ln.Listener.Accept() 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)
}