Skip to content

Commit 6970700

Browse files
committed
Add support for monitoring entire mount
- Add support for monitoring entire mount - Update package documentation - Update README
1 parent b4b8bde commit 6970700

File tree

6 files changed

+141
-52
lines changed

6 files changed

+141
-52
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func main() {
3535
os.Exit(1)
3636
}
3737
mountPoint := "/"
38-
listener, err := fanotify.NewListener(mountPoint)
38+
listener, err := fanotify.NewListener(mountPoint, false)
3939
if err != nil {
4040
fmt.Println(err)
4141
os.Exit(1)

doc.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Package fanotify library provides a simple API to monitor filesystem for events.
2+
//
3+
// The listener is initialized with flags automatically based on the kernel version. The mark flag features that specify the
4+
// the events to monitor a file/directory are validated and checked for valid combinations and validated against the kernel
5+
// version.
6+
//
7+
// fanotify system has features spanning different kernel versions:
8+
// - For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
9+
// - For Linux kernel versions 5.1 to 5.8 additional information about the underlying filesystem object is correlated to an event.
10+
// - For Linux kernel version 5.9 or later the modified file name is made available in the event.
11+
//
12+
package fanotify

doc_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package fanotify_test
2+
3+
import (
4+
"log"
5+
6+
"github.com/opcoder0/fanotify"
7+
)
8+
9+
func ExampleNewListener() {
10+
if _, err := fanotify.NewListener("/", true); err != nil {
11+
log.Fatal("Cannot create listener for mount /", err)
12+
}
13+
}
14+
15+
func ExampleListener_AddWatch() {
16+
var listener *fanotify.Listener
17+
listener, err := fanotify.NewListener("/", false)
18+
if err != nil {
19+
log.Fatal("Cannot create listener for mount /", err)
20+
}
21+
listener.AddWatch("/home/user", fanotify.FileModified)
22+
}
23+
24+
func ExampleListener_AddWatch_all() {
25+
var listener *fanotify.Listener
26+
var actions fanotify.Action
27+
28+
listener, err := fanotify.NewListener("/", false)
29+
if err != nil {
30+
log.Fatal("Cannot create listener for path /", err)
31+
}
32+
actions = fanotify.FileAccessed |
33+
fanotify.FileOrDirectoryAccessed |
34+
fanotify.FileModified |
35+
fanotify.FileOpenedForExec |
36+
fanotify.FileAttribChanged |
37+
fanotify.FileOrDirectoryAttribChanged |
38+
fanotify.FileCreated |
39+
fanotify.FileOrDirectoryCreated |
40+
fanotify.FileDeleted |
41+
fanotify.FileOrDirectoryDeleted |
42+
fanotify.WatchedFileDeleted |
43+
fanotify.WatchedFileOrDirectoryDeleted |
44+
fanotify.FileMovedFrom |
45+
fanotify.FileOrDirectoryMovedFrom |
46+
fanotify.FileMovedTo |
47+
fanotify.FileOrDirectoryMovedTo |
48+
fanotify.WatchedFileMoved |
49+
fanotify.WatchedFileOrDirectoryMoved
50+
listener.AddWatch("/home/user", actions)
51+
}

fanotify_api.go

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
//go:build linux
22
// +build linux
33

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

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

3728
// Action represents an event / operation on a particular file/directory
@@ -65,6 +56,7 @@ type Listener struct {
6556
mountpoint *os.File
6657
kernelMajorVersion int
6758
kernelMinorVersion int
59+
entireMount bool
6860
watches map[string]bool
6961
stopper struct {
7062
r *os.File
@@ -74,40 +66,39 @@ type Listener struct {
7466
Events chan Event
7567
}
7668

77-
// NewListener returns a fanotify listener from which events
69+
// NewListener returns a fanotify listener from which filesystem events
7870
// can be read. Each listener supports listening to events
79-
// under a single mount point.
71+
// under a single mountpoint.
8072
//
8173
// For cases where multiple mountpoints need to be monitored
8274
// multiple listener instances need to be used.
8375
//
84-
// `mountpointPath` can be any file/directory under the mount point being watched.
85-
// `maxEvents` defines the length of the buffered channel which holds the notifications. The minimum length is 4096.
86-
// `withName` setting this to true populates the file name under the watched parent.
76+
// mountpoint can be any file/directory under the mount point being watched.
77+
// Passing "true" to the entireMount flag monitors the entire mount point for marked
78+
// events. Passing "false" allows specifying multiple paths (files/directories)
79+
// under this mount point for monitoring filesystem events.
8780
//
88-
// For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
89-
// For Linux kernel versions 5.1 - 5.8 additional information about the underlying filesystem object is correlated to an event.
90-
// For Linux kernel version 5.9 or later the modified file name is made available in the event.
81+
// The function returns a new instance of the listener. The fanotify flags are set
82+
// based on the running kernel version. ErrCapSysAdmin is returned if the process does not
83+
// have CAP_SYS_ADM capability.
9184
//
92-
// NOTE that this call requires CAP_SYS_ADMIN privilege
93-
func NewListener(mountPoint string) (*Listener, error) {
85+
// - For Linux kernel version 5.0 and earlier no additional information about the underlying filesystem object is available.
86+
// - For Linux kernel versions 5.1 till 5.8 (inclusive) additional information about the underlying filesystem object is correlated to an event.
87+
// - For Linux kernel version 5.9 or later the modified file name is made available in the event.
88+
func NewListener(mountPoint string, entireMount bool) (*Listener, error) {
9489
capSysAdmin, err := checkCapSysAdmin()
9590
if err != nil {
9691
return nil, err
9792
}
9893
if !capSysAdmin {
9994
return nil, ErrCapSysAdmin
10095
}
101-
return newListener(mountPoint)
96+
return newListener(mountPoint, entireMount)
10297
}
10398

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

158-
// AddWatch watches parent directory for specified actions
159-
func (l *Listener) AddWatch(parentDir string, action Action) error {
160-
return l.fanotifyMark(parentDir, unix.FAN_MARK_ADD, uint64(action|unix.FAN_EVENT_ON_CHILD), false)
149+
// MarkMount adds, modifies or removes the fanotify mark (passed in as action) for the entire
150+
// mountpoint. Passing true to remove, removes the mark from the mountpoint.
151+
// This method returns an [ErrWatchPath] if the listener was not initialized to monitor
152+
// the entire mountpoint. To mark specific files or directories use [AddWatch] method.
153+
// The entire mount cannot be monitored for the following events:
154+
// [FileCreated], [FileAttribChanged], [FileMovedFrom],
155+
// [FileMovedTo], [WatchedFileDeleted]
156+
// Passing any of these flags in action will return [ErrInvalidFlagCombination] error
157+
func (l *Listener) MarkMount(action Action, remove bool) error {
158+
if l.entireMount == false {
159+
return ErrWatchPath
160+
}
161+
if action.Has(FileCreated) || action.Has(FileAttribChanged) || action.Has(FileMovedFrom) || action.Has(FileMovedTo) || action.Has(WatchedFileDeleted) {
162+
return ErrInvalidFlagCombination
163+
}
164+
if remove {
165+
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_REMOVE|unix.FAN_MARK_MOUNT, uint64(action), false)
166+
}
167+
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(action), false)
168+
}
169+
170+
// AddWatch adds or modifies the fanotify mark for the specified path.
171+
// The events are only raised for the specified directory and does raise events
172+
// for subdirectories. Calling AddWatch to mark the entire mountpoint results in
173+
// [os.ErrInvalid]. To mark the entire mountpoint use [MarkMount] method.
174+
// Certain flag combinations are known to cause issues.
175+
// - [FileCreated] cannot be or-ed / combined with FileClosed. The fanotify system does not generate any event for this combination.
176+
// - [FileOpened] with any of the actions containing OrDirectory causes an event flood for the directory and then stopping raising any events at all.
177+
// - [FileOrDirectoryOpened] with any of the other actions causes an event flood for the directory and then stopping raising any events at all.
178+
func (l *Listener) AddWatch(path string, action Action) error {
179+
if l.entireMount {
180+
return os.ErrInvalid
181+
}
182+
return l.fanotifyMark(path, unix.FAN_MARK_ADD, uint64(action|unix.FAN_EVENT_ON_CHILD), false)
161183
}
162184

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

168-
// WatchMountPoint watches the entire mount point for specified actions
169-
func (l *Listener) WatchMountPoint(action Action) error {
170-
return l.fanotifyMark(l.mountpoint.Name(), unix.FAN_MARK_ADD|unix.FAN_MARK_MOUNT, uint64(action), false)
171-
}
172-
173195
// ClearWatch stops watching for all actions
174196
func (l *Listener) ClearWatch() error {
175197
if l == nil {

fanotify_event.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func fanotifyEventOK(meta *unix.FanotifyEventMetadata, n int) bool {
191191
int(meta.Event_len) <= n)
192192
}
193193

194-
func newListener(mountpointPath string) (*Listener, error) {
194+
func newListener(mountpointPath string, entireMount bool) (*Listener, error) {
195195

196196
var flags, eventFlags uint
197197

@@ -215,8 +215,11 @@ func newListener(mountpointPath string) (*Listener, error) {
215215
case maj > 5:
216216
flags = unix.FAN_CLASS_NOTIF | unix.FAN_CLOEXEC | unix.FAN_REPORT_DIR_FID | unix.FAN_REPORT_NAME
217217
}
218+
// FAN_MARK_MOUNT cannot be specified with FAN_REPORT_FID, FAN_REPORT_DIR_FID, FAN_REPORT_NAME
219+
if entireMount {
220+
flags = unix.FAN_CLASS_NOTIF | unix.FAN_CLOEXEC
221+
}
218222
eventFlags = unix.O_RDONLY | unix.O_LARGEFILE | unix.O_CLOEXEC
219-
220223
if err := flagsValid(flags); err != nil {
221224
return nil, fmt.Errorf("%w: %v", ErrInvalidFlagCombination, err)
222225
}
@@ -249,6 +252,7 @@ func newListener(mountpointPath string) (*Listener, error) {
249252
mountpoint: mountpoint,
250253
kernelMajorVersion: maj,
251254
kernelMinorVersion: min,
255+
entireMount: entireMount,
252256
watches: make(map[string]bool),
253257
stopper: struct {
254258
r *os.File

fanotify_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func runAsCmd(args ...string) (int, error) {
3939
}
4040

4141
func TestWithCapSysAdmFanotifyFileAccessed(t *testing.T) {
42-
l, err := NewListener("/")
42+
l, err := NewListener("/", false)
4343
assert.Nil(t, err)
4444
assert.NotNil(t, l)
4545
watchDir := t.TempDir()
@@ -67,7 +67,7 @@ func TestWithCapSysAdmFanotifyFileAccessed(t *testing.T) {
6767
}
6868

6969
func TestWithCapSysAdmFanotifyFileModified(t *testing.T) {
70-
l, err := NewListener("/")
70+
l, err := NewListener("/", false)
7171
assert.Nil(t, err)
7272
assert.NotNil(t, l)
7373
watchDir := t.TempDir()
@@ -95,7 +95,7 @@ func TestWithCapSysAdmFanotifyFileModified(t *testing.T) {
9595
}
9696

9797
func TestWithCapSysAdmFanotifyFileClosed(t *testing.T) {
98-
l, err := NewListener("/")
98+
l, err := NewListener("/", false)
9999
assert.Nil(t, err)
100100
assert.NotNil(t, l)
101101
watchDir := t.TempDir()
@@ -123,7 +123,7 @@ func TestWithCapSysAdmFanotifyFileClosed(t *testing.T) {
123123
}
124124

125125
func TestWithCapSysAdmFanotifyFileOpen(t *testing.T) {
126-
l, err := NewListener("/")
126+
l, err := NewListener("/", false)
127127
assert.Nil(t, err)
128128
assert.NotNil(t, l)
129129
watchDir := t.TempDir()
@@ -151,7 +151,7 @@ func TestWithCapSysAdmFanotifyFileOpen(t *testing.T) {
151151
}
152152

153153
func TestWithCapSysAdmFanotifyFileOrDirectoryOpen(t *testing.T) {
154-
l, err := NewListener("/")
154+
l, err := NewListener("/", false)
155155
assert.Nil(t, err)
156156
assert.NotNil(t, l)
157157
watchDir := t.TempDir()
@@ -175,7 +175,7 @@ func TestWithCapSysAdmFanotifyFileOrDirectoryOpen(t *testing.T) {
175175
}
176176

177177
func TestWithCapSysAdmFanotifyFileOpenForExec(t *testing.T) {
178-
l, err := NewListener("/")
178+
l, err := NewListener("/", false)
179179
assert.Nil(t, err)
180180
assert.NotNil(t, l)
181181
watchDir := t.TempDir()
@@ -208,7 +208,7 @@ exit 0
208208
}
209209

210210
func TestWithCapSysAdmFanotifyFileAttribChanged(t *testing.T) {
211-
l, err := NewListener("/")
211+
l, err := NewListener("/", false)
212212
assert.Nil(t, err)
213213
assert.NotNil(t, l)
214214
watchDir := t.TempDir()
@@ -241,7 +241,7 @@ exit 0
241241
}
242242

243243
func TestWithCapSysAdmFanotifyFileCreated(t *testing.T) {
244-
l, err := NewListener("/")
244+
l, err := NewListener("/", false)
245245
assert.Nil(t, err)
246246
assert.NotNil(t, l)
247247
watchDir := t.TempDir()
@@ -265,7 +265,7 @@ func TestWithCapSysAdmFanotifyFileCreated(t *testing.T) {
265265
}
266266

267267
func TestWithCapSysAdmFanotifyFileOrDirectoryCreated(t *testing.T) {
268-
l, err := NewListener("/")
268+
l, err := NewListener("/", false)
269269
assert.Nil(t, err)
270270
assert.NotNil(t, l)
271271
watchDir := t.TempDir()
@@ -290,7 +290,7 @@ func TestWithCapSysAdmFanotifyFileOrDirectoryCreated(t *testing.T) {
290290

291291
func TestWithCapSysAdmFanotifyFileDeleted(t *testing.T) {
292292

293-
l, err := NewListener("/")
293+
l, err := NewListener("/", false)
294294
assert.Nil(t, err)
295295
assert.NotNil(t, l)
296296

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

320320
func TestWithCapSysAdmFanotifyFileOrDirectoryDeleted(t *testing.T) {
321321

322-
l, err := NewListener("/")
322+
l, err := NewListener("/", false)
323323
assert.Nil(t, err)
324324
assert.NotNil(t, l)
325325

@@ -355,7 +355,7 @@ func TestActions(t *testing.T) {
355355
}
356356

357357
func TestMultipleEvents(t *testing.T) {
358-
l, err := NewListener("/")
358+
l, err := NewListener("/", false)
359359
assert.Nil(t, err)
360360
assert.NotNil(t, l)
361361
go l.Start()
@@ -413,7 +413,7 @@ func TestMultipleEvents(t *testing.T) {
413413
// FileCreated and FileClosed combination does not raise any events
414414
func TestWithCapSysAdmMarkCreateCloseBug(t *testing.T) {
415415
if *bug {
416-
l, err := NewListener("/")
416+
l, err := NewListener("/", false)
417417
assert.Nil(t, err)
418418
assert.NotNil(t, l)
419419
go l.Start()
@@ -461,7 +461,7 @@ func TestWithCapSysAdmMarkFileOrDirectoryOpenedBug(t *testing.T) {
461461
assert.Nil(t, err)
462462

463463
// start the listener
464-
l, err := NewListener("/")
464+
l, err := NewListener("/", false)
465465
assert.Nil(t, err)
466466
assert.NotNil(t, l)
467467
go l.Start()

0 commit comments

Comments
 (0)