aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJesse Dunietz2018-10-06 15:49:33 -0400
committerJakob Unterwurzacher2018-10-11 20:16:45 +0200
commit87d3ed9187cb9caccaa8df0769d4cb722cc5bd00 (patch)
tree14963c3f66b1277f0bf7904650665156e102929a
parent57a5a8791f8a8c957aef87aecfa4fa318873b744 (diff)
Add option for autounmount
Even though filesystem notifications aren't implemented for FUSE, I decided to try my hand at implementing the autounmount feature (#128). I based it on the EncFS autounmount code, which records filesystem accesses and checks every X seconds whether it's idled long enough to unmount. I've tested the feature locally, but I haven't added any tests for this flag. I also haven't worked with Go before. So please let me know if there's anything that should be done differently. One particular concern: I worked from the assumption that the open files table is unique per-filesystem. If that's not true, I'll need to add an open file count and associated lock to the Filesystem type instead. https://github.com/rfjakob/gocryptfs/pull/265
-rw-r--r--cli_args.go12
-rw-r--r--help.go1
-rw-r--r--internal/fusefrontend/fs.go5
-rw-r--r--internal/fusefrontend/names.go6
-rw-r--r--internal/openfiletable/open_file_table.go8
-rw-r--r--mount.go72
6 files changed, 91 insertions, 13 deletions
diff --git a/cli_args.go b/cli_args.go
index dd00658..c073958 100644
--- a/cli_args.go
+++ b/cli_args.go
@@ -7,6 +7,7 @@ import (
"os"
"strconv"
"strings"
+ "time"
"github.com/hanwen/go-fuse/fuse"
"github.com/rfjakob/gocryptfs/internal/configfile"
@@ -33,6 +34,8 @@ type argContainer struct {
// Configuration file name override
config string
notifypid, scryptn int
+ // Idle time before autounmount
+ idle time.Duration
// Helper variables that are NOT cli options all start with an underscore
// _configCustom is true when the user sets a custom config file name.
_configCustom bool
@@ -187,6 +190,11 @@ func parseCliOpts() (args argContainer) {
"successful mount - used internally for daemonization")
flagSet.IntVar(&args.scryptn, "scryptn", configfile.ScryptDefaultLogN, "scrypt cost parameter logN. Possible values: 10-28. "+
"A lower value speeds up mounting and reduces its memory needs, but makes the password susceptible to brute-force attacks")
+
+ flagSet.DurationVar(&args.idle, "i", 0, "Alias for -idle")
+ flagSet.DurationVar(&args.idle, "idle", 0, "Auto-unmount after specified idle duration (ignored in reverse mode). "+
+ "Durations are specified like \"500s\" or \"2h45m\". 0 means stay mounted indefinitely.")
+
var dummyString string
flagSet.StringVar(&dummyString, "o", "", "For compatibility with mount(1), options can be also passed as a comma-separated list to -o on the end.")
// Actual parsing
@@ -247,6 +255,10 @@ func parseCliOpts() (args argContainer) {
tlog.Fatal.Printf("The options -extpass and -trezor cannot be used at the same time")
os.Exit(exitcodes.Usage)
}
+ if args.idle < 0 {
+ tlog.Fatal.Printf("Idle timeout cannot be less than 0")
+ os.Exit(exitcodes.Usage)
+ }
return args
}
diff --git a/help.go b/help.go
index 714fcbf..6216a9b 100644
--- a/help.go
+++ b/help.go
@@ -19,6 +19,7 @@ func helpShort() {
Common Options (use -hh to show all):
-aessiv Use AES-SIV encryption (with -init)
-allow_other Allow other users to access the mount
+ -i, -idle Unmount automatically after specified idle duration
-config Custom path to config file
-ctlsock Create control socket at location
-extpass Call external program to prompt for the password
diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go
index 1b6941e..c0d6151 100644
--- a/internal/fusefrontend/fs.go
+++ b/internal/fusefrontend/fs.go
@@ -47,6 +47,11 @@ type FS struct {
// "gocryptfs -fsck" reads from the channel to also catch these transparently-
// mitigated corruptions.
MitigatedCorruptions chan string
+ // Track accesses to the filesystem so that we can know when to autounmount.
+ // An access is considered to have happened on every call to encryptPath,
+ // which is called as part of every filesystem operation.
+ // (This flag uses a uint32 so that it can be reset with CompareAndSwapUint32.)
+ AccessedSinceLastCheck uint32
}
var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented.
diff --git a/internal/fusefrontend/names.go b/internal/fusefrontend/names.go
index 5530e3e..3bf64d5 100644
--- a/internal/fusefrontend/names.go
+++ b/internal/fusefrontend/names.go
@@ -60,9 +60,15 @@ func (fs *FS) openBackingDir(relPath string) (dirfd int, cName string, err error
// encryptPath - encrypt relative plaintext path
func (fs *FS) encryptPath(plainPath string) (string, error) {
+ if plainPath != "" { // Empty path gets encrypted all the time without actual file accesses.
+ fs.AccessedSinceLastCheck = 1
+ } else { // Empty string gets encrypted as empty string
+ return plainPath, nil
+ }
if fs.args.PlaintextNames {
return plainPath, nil
}
+
fs.dirIVLock.RLock()
cPath, err := fs.nameTransform.EncryptPathDirIV(plainPath, fs.args.Cipherdir)
tlog.Debug.Printf("encryptPath '%s' -> '%s' (err: %v)", plainPath, cPath, err)
diff --git a/internal/openfiletable/open_file_table.go b/internal/openfiletable/open_file_table.go
index e21c96d..4a8ce28 100644
--- a/internal/openfiletable/open_file_table.go
+++ b/internal/openfiletable/open_file_table.go
@@ -112,3 +112,11 @@ func (c *countingMutex) Lock() {
func WriteOpCount() uint64 {
return atomic.LoadUint64(&t.writeOpCount)
}
+
+// CountOpenFiles returns how many entries are currently in the table
+// in a threadsafe manner.
+func CountOpenFiles() int {
+ t.Lock()
+ defer t.Unlock()
+ return len(t.entries)
+}
diff --git a/mount.go b/mount.go
index f473e1e..b4b6e61 100644
--- a/mount.go
+++ b/mount.go
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"log/syslog"
+ "math"
"net"
"os"
"os/exec"
@@ -14,6 +15,7 @@ import (
"runtime"
"runtime/debug"
"strings"
+ "sync/atomic"
"syscall"
"time"
@@ -29,6 +31,7 @@ import (
"github.com/rfjakob/gocryptfs/internal/fusefrontend"
"github.com/rfjakob/gocryptfs/internal/fusefrontend_reverse"
"github.com/rfjakob/gocryptfs/internal/nametransform"
+ "github.com/rfjakob/gocryptfs/internal/openfiletable"
"github.com/rfjakob/gocryptfs/internal/tlog"
)
@@ -98,7 +101,7 @@ func doMount(args *argContainer) {
fs, wipeKeys := initFuseFrontend(args)
// Initialize go-fuse FUSE server
srv := initGoFuse(fs, args)
- // Try to wipe secrect keys from memory after unmount
+ // Try to wipe secret keys from memory after unmount
defer wipeKeys()
tlog.Info.Println(tlog.ColorGreen + "Filesystem mounted and ready." + tlog.ColorReset)
@@ -137,10 +140,49 @@ func doMount(args *argContainer) {
// Return memory that was allocated for scrypt (64M by default!) and other
// stuff that is no longer needed to the OS
debug.FreeOSMemory()
+ // Set up autounmount, if requested.
+ if args.idle > 0 && !args.reverse {
+ // Not being in reverse mode means we always have a forward file system.
+ fwdFs := fs.(*fusefrontend.FS)
+ go idleMonitor(args.idle, fwdFs, srv, args.mountpoint)
+ }
// Jump into server loop. Returns when it gets an umount request from the kernel.
srv.Serve()
}
+// Based on the EncFS idle monitor:
+// https://github.com/vgough/encfs/blob/1974b417af189a41ffae4c6feb011d2a0498e437/encfs/main.cpp#L851
+// idleMonitor is a function to be run as a thread that checks for
+// filesystem idleness and unmounts if we've been idle for long enough.
+const checksDuringTimeoutPeriod = 4
+
+func idleMonitor(idleTimeout time.Duration, fs *fusefrontend.FS, srv *fuse.Server, mountpoint string) {
+ sleepTimeBetweenChecks := contentenc.MinUint64(
+ uint64(idleTimeout/checksDuringTimeoutPeriod),
+ uint64(2*time.Minute))
+ timeoutCycles := int(math.Ceil(float64(idleTimeout) / float64(sleepTimeBetweenChecks)))
+ idleCount := 0
+ for {
+ // Atomically check whether the access flag is set and reset it to 0 if so.
+ recentAccess := atomic.CompareAndSwapUint32(&fs.AccessedSinceLastCheck, 1, 0)
+ // Any form of current or recent access resets the idle counter.
+ openFileCount := openfiletable.CountOpenFiles()
+ if recentAccess || openFileCount > 0 {
+ idleCount = 0
+ } else {
+ idleCount++
+ }
+ tlog.Debug.Printf(
+ "Checking for idle (recentAccess = %t, open = %d): %s",
+ recentAccess, openFileCount, time.Now().String())
+ if idleCount > 0 && idleCount%timeoutCycles == 0 {
+ tlog.Info.Printf("Filesystem idle; unmounting: %s", mountpoint)
+ unmount(srv, mountpoint)
+ }
+ time.Sleep(time.Duration(sleepTimeBetweenChecks))
+ }
+}
+
// setOpenFileLimit tries to increase the open file limit to 4096 (the default hard
// limit on Linux).
func setOpenFileLimit() {
@@ -379,18 +421,22 @@ func handleSigint(srv *fuse.Server, mountpoint string) {
signal.Notify(ch, syscall.SIGTERM)
go func() {
<-ch
- err := srv.Unmount()
- if err != nil {
- tlog.Warn.Printf("handleSigint: srv.Unmount returned %v", err)
- if runtime.GOOS == "linux" {
- // MacOSX does not support lazy unmount
- tlog.Info.Printf("Trying lazy unmount")
- cmd := exec.Command("fusermount", "-u", "-z", mountpoint)
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Run()
- }
- }
+ unmount(srv, mountpoint)
os.Exit(exitcodes.SigInt)
}()
}
+
+func unmount(srv *fuse.Server, mountpoint string) {
+ err := srv.Unmount()
+ if err != nil {
+ tlog.Warn.Printf("unmount: srv.Unmount returned %v", err)
+ if runtime.GOOS == "linux" {
+ // MacOSX does not support lazy unmount
+ tlog.Info.Printf("Trying lazy unmount")
+ cmd := exec.Command("fusermount", "-u", "-z", mountpoint)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ cmd.Run()
+ }
+ }
+}