Repository: fail2ban/fail2ban

Issue:
PyInotify Issue - Occoured while Logrotate
After a hard 'yesterday' I got the new Version 0.9.0 to properly work. (Had to create a paths-overrides.local and a prepared "paths-gentoo-hardened.local" in what I edited some of the others, like paths-debian and so on, but for my system. So far, so good, f2b started properly, did some tests (attacked my own server with different, that should be matched to the filters) - worked...

But Today I found in the "/var/log/fail2ban.log" This:

cat /var/log/fail2ban.log
2014-03-19 03:10:02,681 fail2ban.server.server[16468]: INFO    rollover performed on /var/log/fail2ban.log
2014-03-19 03:10:03,572 pyinotify       [16468]: ERROR   rm_watch: cannot remove WD=2, Errno=Invalid argument (EINVAL)
2014-03-19 03:10:03,708 pyinotify       [16468]: ERROR   rm_watch: cannot remove WD=2, Errno=Invalid argument (EINVAL)
2014-03-19 18:48:13,709 fail2ban.server.filter[16468]: INFO    Log rotation detected for /var/log/auth.log
2014-03-19 18:48:13,713 fail2ban.server.filter[16468]: INFO    Log rotation detected for /var/log/auth.log

Could anybody tell me, what this causes? These two "ERROR"-Lines?
What do these mean?

The logrotate works properly, too: A new "fail2ban.log" was created, the old from yesterday got archieved.

Thanks for anyone for help :)
GCC1977



Discussion:
Thanks for the report @GCC1977. The _pyinotify_ filter monitors both files and the directories the files are in. I assume you have at least one log file your monitoring in `/var/log/`, hence why _pyinotify_ has flagged up on log rotate in that folder. I'm not sure why those errors are coming up, but they are originating from pyinotify…

What version of pyinotify do you have installed?

Hello kwirk :)

The error didnt occour for 2 days and then appears again in yesterday fail2ban.log under /var/log.
Interresting, why it was not for the 2 days... I didn't change anything in the f2b-config-files, didn't related to f2b/pyinotify files/no updates/upgrades installed.

On my system, the Version "0.9.4-r1" is installed:
eix -e pyinotify
[I] dev-python/pyinotify
     Available versions:  0.9.2 ~0.9.3 ~0.9.4 0.9.4-r1 {examples PYTHON_TARGETS="pypy2_0 python2_6 python2_7 python3_2 python3_3"}
     Installed versions:  0.9.4-r1(21:47:37 12.02.2014)(-examples PYTHON_TARGETS="python2_7 python3_3 -pypy2_0 -python2_6 -python3_2")
     Homepage:            http://trac.dbzteam.org/pyinotify http://pypi.python.org/pypi/pyinotify
     Description:         Python module used for monitoring filesystems events

Thanks for your help and explanation, kwirk! :)

Does appear to be a bug in Fail2Ban. When a file is deleted, the watcher for it is automatically removed. `FilterPyinotify` is detecting a new file being created with the same name, and in turn attempts to remove the watch for the deleted file (which has already been removed automatically).

Also, IN_IGNORED events are generated when a file is deleted, but these weren't being caught, causing an non-existent file path to be passed to `FilterPyinotify._process_file` (which caught the exceptions in `Filter.getFailures`). I guess this hasn't been noticed before, as most events from things like logrotate are file moves rather than removals.

@yarikoptic & @grooverdan : Propose patch below.

``` diff
diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py
index b340ef1..ef0d245 100644
--- a/fail2ban/server/filterpyinotify.py
+++ b/fail2ban/server/filterpyinotify.py
@@ -91,6 +91,9 @@ class FilterPyinotify(FileFilter):
                self._delFileWatcher(path)
                # place a new one
                self._addFileWatcher(path)
+       elif event.mask & pyinotify.IN_IGNORED:
+           # Caused when watch removed, either by rm_watch or file deletion
+           return

        self._process_file(path)

@@ -119,9 +122,11 @@ class FilterPyinotify(FileFilter):

    def _delFileWatcher(self, path):
        wdInt = self.__watches[path]
-       wd = self.__monitor.rm_watch(wdInt)
-       if wd[wdInt]:
-           del self.__watches[path]
+       if wdInt is None:
+           return False
+       elif self.__monitor.get_path(wdInt) is None or \
+           self.__monitor.rm_watch(wdInt)[wdInt]:
+           self.__watches[path] = None
            logSys.debug("Removed file watcher for %s", path)
            return True
        else:
@@ -152,6 +157,7 @@ class FilterPyinotify(FileFilter):
    def _delLogPath(self, path):
        if not self._delFileWatcher(path):
            logSys.error("Failed to remove watch on path: %s", path)
+       del self.__watches[path]

        path_dir = dirname(path)
        if not len([k for k in self.__watches
```

looks reasonable

looks ok to me but...
ideally this should be a PR so we could see how travis reacts ;)
also I am surprised that we do not have (?) yet unittests for such a case -- I thought we had those covered quite densely, but I guess I was wrong.

and I guess we would need to backport it to 0.8.x


Resolving patch:
diff --git a/server/filterpyinotify.py b/server/filterpyinotify.py
--- a/server/filterpyinotify.py
+++ b/server/filterpyinotify.py
@@ -90,6 +90,9 @@ def callback(self, event, origin=''):
 				self._delFileWatcher(path)
 				# place a new one
 				self._addFileWatcher(path)
+		elif event.mask & pyinotify.IN_IGNORED:
+			# Caused when watch removed, either by rm_watch or file deletion
+			return
 
 		self._process_file(path)
 
@@ -118,9 +121,11 @@ def _addFileWatcher(self, path):
 
 	def _delFileWatcher(self, path):
 		wdInt = self.__watches[path]
-		wd = self.__monitor.rm_watch(wdInt)
-		if wd[wdInt]:
-			del self.__watches[path]
+		if wdInt is None:
+			return False
+		elif self.__monitor.get_path(wdInt) is None or \
+			self.__monitor.rm_watch(wdInt)[wdInt]:
+			self.__watches[path] = None
 			logSys.debug("Removed file watcher for %s", path)
 			return True
 		else:
@@ -151,6 +156,7 @@ def _addLogPath(self, path):
 	def _delLogPath(self, path):
 		if not self._delFileWatcher(path):
 			logSys.error("Failed to remove watch on path: %s", path)
+		del self.__watches[path]
 
 		path_dir = dirname(path)
 		if not len([k for k in self.__watches


Test patch: