Skip to content

Commit

Permalink
Add support for monitoring entire mount
Browse files Browse the repository at this point in the history
- Add support for monitoring entire mount
- Update package documentation
- Update README
  • Loading branch information
opcoder0 committed Dec 9, 2022
1 parent b4b8bde commit 6970700
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 52 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func main() {
os.Exit(1)
}
mountPoint := "/"
listener, err := fanotify.NewListener(mountPoint)
listener, err := fanotify.NewListener(mountPoint, false)
if err != nil {
fmt.Println(err)
os.Exit(1)
Expand Down
12 changes: 12 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Package fanotify library provides a simple API to monitor filesystem for events.
//
// The listener is initialized with flags automatically based on the kernel version. The mark flag features that specify the
// the events to monitor a file/directory are validated and checked for valid combinations and validated against the kernel
// version.
//
// fanotify system has features spanning different kernel versions:
// - For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
// - For Linux kernel versions 5.1 to 5.8 additional information about the underlying filesystem object is correlated to an event.
// - For Linux kernel version 5.9 or later the modified file name is made available in the event.
//
package fanotify
51 changes: 51 additions & 0 deletions doc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fanotify_test

import (
"log"

"github.com/opcoder0/fanotify"
)

func ExampleNewListener() {
if _, err := fanotify.NewListener("/", true); err != nil {
log.Fatal("Cannot create listener for mount /", err)
}
}

func ExampleListener_AddWatch() {
var listener *fanotify.Listener
listener, err := fanotify.NewListener("/", false)
if err != nil {
log.Fatal("Cannot create listener for mount /", err)
}
listener.AddWatch("/home/user", fanotify.FileModified)
}

func ExampleListener_AddWatch_all() {
var listener *fanotify.Listener
var actions fanotify.Action

listener, err := fanotify.NewListener("/", false)
if err != nil {
log.Fatal("Cannot create listener for path /", err)
}
actions = fanotify.FileAccessed |
fanotify.FileOrDirectoryAccessed |
fanotify.FileModified |
fanotify.FileOpenedForExec |
fanotify.FileAttribChanged |
fanotify.FileOrDirectoryAttribChanged |
fanotify.FileCreated |
fanotify.FileOrDirectoryCreated |
fanotify.FileDeleted |
fanotify.FileOrDirectoryDeleted |
fanotify.WatchedFileDeleted |
fanotify.WatchedFileOrDirectoryDeleted |
fanotify.FileMovedFrom |
fanotify.FileOrDirectoryMovedFrom |
fanotify.FileMovedTo |
fanotify.FileOrDirectoryMovedTo |
fanotify.WatchedFileMoved |
fanotify.WatchedFileOrDirectoryMoved
listener.AddWatch("/home/user", actions)
}
92 changes: 57 additions & 35 deletions fanotify_api.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
//go:build linux
// +build linux

// Package fanotify library provides a simple API to monitor filesystem for events.
//
// The listener is initialized with flags automatically based on the kernel version. The mark flag features that specify the
// the events to monitor a file/directory are validated and checked for valid combinations and validated against the kernel
// version.
//
// fanotify has features spanning different kernel versions -
//
// For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
// For Linux kernel versions 5.1 - 5.8 additional information about the underlying filesystem object is correlated to an event.
// For Linux kernel version 5.9 or later the modified file name is made available in the event.
package fanotify

import (
Expand All @@ -32,6 +21,8 @@ var (
ErrNilListener = errors.New("nil listener")
// ErrUnsupportedOnKernelVersion indicates the feature/flag is unavailable for the current kernel version
ErrUnsupportedOnKernelVersion = errors.New("feature unsupported on current kernel version")
// ErrWatchPath indicates path needs to be specified for watching
ErrWatchPath = errors.New("missing watch path")
)

// Action represents an event / operation on a particular file/directory
Expand Down Expand Up @@ -65,6 +56,7 @@ type Listener struct {
mountpoint *os.File
kernelMajorVersion int
kernelMinorVersion int
entireMount bool
watches map[string]bool
stopper struct {
r *os.File
Expand All @@ -74,40 +66,39 @@ type Listener struct {
Events chan Event
}

// NewListener returns a fanotify listener from which events
// NewListener returns a fanotify listener from which filesystem events
// can be read. Each listener supports listening to events
// under a single mount point.
// under a single mountpoint.
//
// For cases where multiple mountpoints need to be monitored
// multiple listener instances need to be used.
//
// `mountpointPath` can be any file/directory under the mount point being watched.
// `maxEvents` defines the length of the buffered channel which holds the notifications. The minimum length is 4096.
// `withName` setting this to true populates the file name under the watched parent.
// mountpoint can be any file/directory under the mount point being watched.
// Passing "true" to the entireMount flag monitors the entire mount point for marked
// events. Passing "false" allows specifying multiple paths (files/directories)
// under this mount point for monitoring filesystem events.
//
// For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
// For Linux kernel versions 5.1 - 5.8 additional information about the underlying filesystem object is correlated to an event.
// For Linux kernel version 5.9 or later the modified file name is made available in the event.
// The function returns a new instance of the listener. The fanotify flags are set
// based on the running kernel version. ErrCapSysAdmin is returned if the process does not
// have CAP_SYS_ADM capability.
//
// NOTE that this call requires CAP_SYS_ADMIN privilege
func NewListener(mountPoint string) (*Listener, error) {
// - For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
// - For Linux kernel versions 5.1 till 5.8 (inclusive) additional information about the underlying filesystem object is correlated to an event.
// - For Linux kernel version 5.9 or later the modified file name is made available in the event.
func NewListener(mountPoint string, entireMount bool) (*Listener, error) {
capSysAdmin, err := checkCapSysAdmin()
if err != nil {
return nil, err
}
if !capSysAdmin {
return nil, ErrCapSysAdmin
}
return newListener(mountPoint)
return newListener(mountPoint, entireMount)
}

// Start starts the listener and polls the fanotify event notification group for marked events.
// The events are pushed into the Listener's `Events` buffered channel.
// The function panics if there nothing to watch.
func (l *Listener) Start() {
//if len(l.watches) == 0 {
// panic("Nothing to watch. Add Directory/File to the listener to watch")
//}
var fds [2]unix.PollFd
// Fanotify Fd
fds[0].Fd = int32(l.fd)
Expand Down Expand Up @@ -155,21 +146,52 @@ func (l *Listener) Stop() {
close(l.Events)
}

// AddWatch watches parent directory for specified actions
func (l *Listener) AddWatch(parentDir string, action Action) error {
return l.fanotifyMark(parentDir, unix.FAN_MARK_ADD, uint64(action|unix.FAN_EVENT_ON_CHILD), false)
// MarkMount adds, modifies or removes the fanotify mark (passed in as action) for the entire
// mountpoint. Passing true to remove, removes the mark from the mountpoint.
// This method returns an [ErrWatchPath] if the listener was not initialized to monitor
// the entire mountpoint. To mark specific files or directories use [AddWatch] method.
// The entire mount cannot be monitored for the following events:
// [FileCreated], [FileAttribChanged], [FileMovedFrom],
// [FileMovedTo], [WatchedFileDeleted]
// Passing any of these flags in action will return [ErrInvalidFlagCombination] error
func (l *Listener) MarkMount(action Action, remove bool) error {
if l.entireMount == false {
return ErrWatchPath
}
if action.Has(FileCreated) || action.Has(FileAttribChanged) || action.Has(FileMovedFrom) || action.Has(FileMovedTo) || action.Has(WatchedFileDeleted) {
return ErrInvalidFlagCombination
}
if remove {
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_REMOVE|unix.FAN_MARK_MOUNT, uint64(action), false)
}
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(action), false)
}

// AddWatch adds or modifies the fanotify mark for the specified path.
// The events are only raised for the specified directory and does raise events
// for subdirectories. Calling AddWatch to mark the entire mountpoint results in
// [os.ErrInvalid]. To mark the entire mountpoint use [MarkMount] method.
// Certain flag combinations are known to cause issues.
// - [FileCreated] cannot be or-ed / combined with FileClosed. The fanotify system does not generate any event for this combination.
// - [FileOpened] with any of the actions containing OrDirectory causes an event flood for the directory and then stopping raising any events at all.
// - [FileOrDirectoryOpened] with any of the other actions causes an event flood for the directory and then stopping raising any events at all.
func (l *Listener) AddWatch(path string, action Action) error {
if l.entireMount {
return os.ErrInvalid
}
return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(action|unix.FAN_EVENT_ON_CHILD), false)
}

// DeleteWatch stops watching the parent directory for the specified action
// DeleteWatch removes or modifies the fanotify mark for the specified path.
// Calling DeleteWatch on the listener initialized to monitor the entire mountpoint
// results in [os.ErrInvalid]. To modify the mark for the entire mountpoint use [MarkMount] method.
func (l *Listener) DeleteWatch(parentDir string, action Action) error {
if l.entireMount {
return os.ErrInvalid
}
return l.fanotifyMark(parentDir, unix.FAN_MARK_REMOVE, uint64(action|unix.FAN_EVENT_ON_CHILD), false)
}

// WatchMountPoint watches the entire mount point for specified actions
func (l *Listener) WatchMountPoint(action Action) error {
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(action), false)
}

// ClearWatch stops watching for all actions
func (l *Listener) ClearWatch() error {
if l == nil {
Expand Down
8 changes: 6 additions & 2 deletions fanotify_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ func fanotifyEventOK(meta *unix.FanotifyEventMetadata, n int) bool {
int(meta.Event_len) <= n)
}

func newListener(mountpointPath string) (*Listener, error) {
func newListener(mountpointPath string, entireMount bool) (*Listener, error) {

var flags, eventFlags uint

Expand All @@ -215,8 +215,11 @@ func newListener(mountpointPath string) (*Listener, error) {
case maj > 5:
flags = unix.FAN_CLASS_NOTIF | unix.FAN_CLOEXEC | unix.FAN_REPORT_DIR_FID | unix.FAN_REPORT_NAME
}
// FAN_MARK_MOUNT cannot be specified with FAN_REPORT_FID, FAN_REPORT_DIR_FID, FAN_REPORT_NAME
if entireMount {
flags = unix.FAN_CLASS_NOTIF | unix.FAN_CLOEXEC
}
eventFlags = unix.O_RDONLY | unix.O_LARGEFILE | unix.O_CLOEXEC

if err := flagsValid(flags); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidFlagCombination, err)
}
Expand Down Expand Up @@ -249,6 +252,7 @@ func newListener(mountpointPath string) (*Listener, error) {
mountpoint: mountpoint,
kernelMajorVersion: maj,
kernelMinorVersion: min,
entireMount: entireMount,
watches: make(map[string]bool),
stopper: struct {
r *os.File
Expand Down
28 changes: 14 additions & 14 deletions fanotify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func runAsCmd(args ...string) (int, error) {
}

func TestWithCapSysAdmFanotifyFileAccessed(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestWithCapSysAdmFanotifyFileAccessed(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileModified(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestWithCapSysAdmFanotifyFileModified(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileClosed(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -123,7 +123,7 @@ func TestWithCapSysAdmFanotifyFileClosed(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileOpen(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -151,7 +151,7 @@ func TestWithCapSysAdmFanotifyFileOpen(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileOrDirectoryOpen(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand All @@ -175,7 +175,7 @@ func TestWithCapSysAdmFanotifyFileOrDirectoryOpen(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileOpenForExec(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -208,7 +208,7 @@ exit 0
}

func TestWithCapSysAdmFanotifyFileAttribChanged(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand Down Expand Up @@ -241,7 +241,7 @@ exit 0
}

func TestWithCapSysAdmFanotifyFileCreated(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand All @@ -265,7 +265,7 @@ func TestWithCapSysAdmFanotifyFileCreated(t *testing.T) {
}

func TestWithCapSysAdmFanotifyFileOrDirectoryCreated(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
watchDir := t.TempDir()
Expand All @@ -290,7 +290,7 @@ func TestWithCapSysAdmFanotifyFileOrDirectoryCreated(t *testing.T) {

func TestWithCapSysAdmFanotifyFileDeleted(t *testing.T) {

l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)

Expand Down Expand Up @@ -319,7 +319,7 @@ func TestWithCapSysAdmFanotifyFileDeleted(t *testing.T) {

func TestWithCapSysAdmFanotifyFileOrDirectoryDeleted(t *testing.T) {

l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)

Expand Down Expand Up @@ -355,7 +355,7 @@ func TestActions(t *testing.T) {
}

func TestMultipleEvents(t *testing.T) {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
go l.Start()
Expand Down Expand Up @@ -413,7 +413,7 @@ func TestMultipleEvents(t *testing.T) {
// FileCreated and FileClosed combination does not raise any events
func TestWithCapSysAdmMarkCreateCloseBug(t *testing.T) {
if *bug {
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
go l.Start()
Expand Down Expand Up @@ -461,7 +461,7 @@ func TestWithCapSysAdmMarkFileOrDirectoryOpenedBug(t *testing.T) {
assert.Nil(t, err)

// start the listener
l, err := NewListener("/")
l, err := NewListener("/", false)
assert.Nil(t, err)
assert.NotNil(t, l)
go l.Start()
Expand Down

0 comments on commit 6970700

Please sign in to comment.