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>
pull/3532/head
Brad Fitzpatrick 3 years ago committed by Brad Fitzpatrick
parent 190b7a4cca
commit abc00e9c8d

@ -142,7 +142,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().
@ -234,6 +238,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 {
@ -2199,10 +2214,11 @@ func (b *LocalBackend) initPeerAPIListener() {
} }
ps := &peerAPIServer{ ps := &peerAPIServer{
b: b, b: b,
rootDir: fileRoot, rootDir: fileRoot,
selfNode: selfNode, selfNode: selfNode,
directFileMode: b.directFileRoot != "", directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
} }
if re, ok := b.e.(wgengine.ResolvingEngine); ok { if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok { if r, ok := re.GetResolver(); ok {

@ -56,10 +56,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 (
@ -697,7 +704,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()
} }

@ -759,6 +759,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)
@ -1114,3 +1126,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)
}

Loading…
Cancel
Save