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
This commit is contained in:
parent
57a5a8791f
commit
87d3ed9187
12
cli_args.go
12
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
|
||||
}
|
||||
|
||||
|
1
help.go
1
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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
72
mount.go
72
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user