[packages/fail2ban] dopisz testy do łatek lag-visibility/restore/gil/idle-gate 7 testów w suitach upstreamu; setswitchin

arekm arekm at pld-linux.org
Wed Jul 22 17:47:58 CEST 2026


commit 74db76d97f2db8dcde983b6ec2568841c1aacf22
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Wed Jul 22 16:59:33 2026 +0200

    dopisz testy do łatek lag-visibility/restore/gil/idle-gate
    7 testów w suitach upstreamu; setswitchinterval przeniesiony do Server.__init__, żeby był testowalny (efekt identyczny)

 fail2ban-gil-switchinterval.patch    | 40 +++++++++++++-----
 fail2ban-lag-visibility.patch        | 61 +++++++++++++++++++++++++---
 fail2ban-restore-before-filter.patch | 75 +++++++++++++++++++++++++++++++++-
 fail2ban-restore-idle-gate.patch     | 79 ++++++++++++++++++++++++++++++++++--
 4 files changed, 233 insertions(+), 22 deletions(-)
---
diff --git a/fail2ban-gil-switchinterval.patch b/fail2ban-gil-switchinterval.patch
index 7c7eaf5..7a622f6 100644
--- a/fail2ban-gil-switchinterval.patch
+++ b/fail2ban-gil-switchinterval.patch
@@ -1,16 +1,36 @@
 diff -ru a/fail2ban/server/server.py b/fail2ban/server/server.py
---- a/fail2ban/server/server.py	2026-07-22 10:03:43.714857606 +0200
-+++ b/fail2ban/server/server.py	2026-07-22 10:03:43.715214163 +0200
-@@ -138,6 +138,12 @@
- 
- 		logSys.info("-"*50)
- 		logSys.info("Starting Fail2ban v%s", version.version)
-+
+--- a/fail2ban/server/server.py	2026-07-22 16:52:08.718289213 +0200
++++ b/fail2ban/server/server.py	2026-07-22 16:52:08.719121840 +0200
+@@ -93,6 +93,11 @@
+ 			'Linux': '/dev/log',
+ 		}
+ 		self.__prev_signals = {}
 +		# The banning thread releases the GIL for every action step (netlink,
 +		# exec) and with the default switch interval (5ms) it gets starved by
 +		# CPU-bound filter threads on log backlog/flood, so bans lag minutes
 +		# behind matches; a shorter interval speeds up GIL hand-over (~15x).
 +		sys.setswitchinterval(1e-3)
- 		
- 		if self.__daemon: # pragma: no cover
- 			logSys.info("Daemon started")
+ 
+ 	def __sigTERMhandler(self, signum, frame): # pragma: no cover - indirect tested
+ 		logSys.debug("Caught signal %d. Exiting", signum)
+diff -ru a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py
+--- a/fail2ban/tests/servertestcase.py	2026-07-22 16:52:08.718482886 +0200
++++ b/fail2ban/tests/servertestcase.py	2026-07-22 16:52:08.719335982 +0200
+@@ -294,6 +294,17 @@
+ 		self.assertNotIn(self.jailName, self.server._Server__jails)
+ 		self.assertNotIn("TestJail2", self.server._Server__jails)
+ 
++	def testServerInitSetsSwitchInterval(self):
++		# creating the server tightens the GIL switch interval (server.py);
++		# process-wide, hence saved and restored around the check
++		orig = sys.getswitchinterval()
++		try:
++			sys.setswitchinterval(0.005)
++			Server()
++			self.assertEqual(sys.getswitchinterval(), 1e-3)
++		finally:
++			sys.setswitchinterval(orig)
++
+ 	def testJailIdle(self):
+ 		self.assertEqual(
+ 			self.transm.proceed(["set", self.jailName, "idle", "on"]),
diff --git a/fail2ban-lag-visibility.patch b/fail2ban-lag-visibility.patch
index 6d707b3..d7fb5d8 100644
--- a/fail2ban-lag-visibility.patch
+++ b/fail2ban-lag-visibility.patch
@@ -1,6 +1,6 @@
 diff -ru a/fail2ban/server/actions.py b/fail2ban/server/actions.py
---- a/fail2ban/server/actions.py	2026-07-22 15:52:12.372829655 +0200
-+++ b/fail2ban/server/actions.py	2026-07-22 15:52:12.374253597 +0200
+--- a/fail2ban/server/actions.py	2026-07-22 16:51:43.545466291 +0200
++++ b/fail2ban/server/actions.py	2026-07-22 16:51:43.547515519 +0200
 @@ -503,7 +503,10 @@
  				# report ticket to observer, to check time should be increased and hereafter observer writes ban to database (asynchronous)
  				if Observers.Main is not None and not bTicket.restored:
@@ -26,8 +26,8 @@ diff -ru a/fail2ban/server/actions.py b/fail2ban/server/actions.py
  					if bTicket.banEpoch == self.banEpoch and diftm > 3:
  						# avoid too often checks:
 diff -ru a/fail2ban/server/filter.py b/fail2ban/server/filter.py
---- a/fail2ban/server/filter.py	2026-07-22 15:52:12.373036499 +0200
-+++ b/fail2ban/server/filter.py	2026-07-22 15:52:12.374691282 +0200
+--- a/fail2ban/server/filter.py	2026-07-22 16:51:43.545668682 +0200
++++ b/fail2ban/server/filter.py	2026-07-22 16:51:43.547937305 +0200
 @@ -606,12 +606,12 @@
  		if self.__ignoreCache: c.set(key, False)
  		return False
@@ -69,9 +69,34 @@ diff -ru a/fail2ban/server/filter.py b/fail2ban/server/filter.py
  					# simulate now as date:
  					date = MyTime.time()
  					self.__lastDate = date
+diff -ru a/fail2ban/tests/actionstestcase.py b/fail2ban/tests/actionstestcase.py
+--- a/fail2ban/tests/actionstestcase.py	2026-07-22 16:51:43.546046910 +0200
++++ b/fail2ban/tests/actionstestcase.py	2026-07-22 16:51:43.548752286 +0200
+@@ -87,6 +87,21 @@
+ 		self.assertLogged('Ban 192.0.2.2')
+ 		self.assertLogged('Ban 192.0.2.3')
+ 
++	@with_alt_time
++	def testBanAgeAppended(self):
++		# age = how far behind real-time the triggering failure is: fresh
++		# bans of a backlog show a big age, an "already banned" IP that
++		# still hits the log shows a small age (ban possibly not effective)
++		self.__jail.putFailTicket(FailTicket('192.0.2.9', MyTime.time() - 65))
++		self.__actions.start()
++		try:
++			self.assertLogged('Ban 192.0.2.9 (age 65s)', wait=True)
++			self.__jail.putFailTicket(FailTicket('192.0.2.9', MyTime.time() - 10))
++			self.assertLogged('192.0.2.9 already banned (age 10s)', wait=True)
++		finally:
++			self.__actions.stop()
++			self.__actions.join()
++
+ 	def testActionsOutput(self):
+ 		self.defaultAction()
+ 		self.__actions.start()
 diff -ru a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py
---- a/fail2ban/tests/filtertestcase.py	2026-07-22 15:52:12.373231343 +0200
-+++ b/fail2ban/tests/filtertestcase.py	2026-07-22 15:52:12.375178158 +0200
+--- a/fail2ban/tests/filtertestcase.py	2026-07-22 16:51:43.545858621 +0200
++++ b/fail2ban/tests/filtertestcase.py	2026-07-22 16:51:43.548601218 +0200
 @@ -471,6 +471,7 @@
  				"Total # of detected failures: 3.", all=True, wait=True)
  			#
@@ -88,3 +113,27 @@ diff -ru a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py
  				self.pruneLog('[phase {phase}] log entries offset by {delta}s'.format(phase=phase, delta=delta))
  				self.filter.processLineAndAdd('2019-10-27 02:00:00 fail from 192.0.2.15');
  				self.assertLogged("Found 192.0.2.15", wait=True)
+@@ -515,6 +517,23 @@
+ 		finally:
+ 			tearDownMyTime()
+ 
++	def testWrongTimeBehindNotSilencedByAhead(self):
++		# entries ahead of the clock (timezone, 24h throttle) must not arm
++		# silence for behind-the-clock entries (60s throttle, separate key)
++		try:
++			self.filter.addFailRegex('fail from <ADDR>$')
++			self.filter.setDatePattern(r'{^LN-BEG}%Y-%m-%d %H:%M:%S(?:\s*%Z)?\s')
++			self.filter.setMaxRetry(50)
++			self.filter.inOperation = True
++			# Mon 2019-10-28 12:00:00 CET (away from the DST switch)
++			MyTime.setTime(1572260400)
++			self.filter.processLineAndAdd('2019-10-28 14:00:00 fail from 192.0.2.11'); # 2h ahead
++			self.assertLogged("timezone problem", all=True)
++			self.filter.processLineAndAdd('2019-10-28 11:10:00 fail from 192.0.2.11'); # 50min behind
++			self.assertLogged("latency problem", all=True)
++		finally:
++			tearDownMyTime()
++
+ 	def testAddAttempt(self):
+ 		self.filter.setMaxRetry(3)
+ 		for i in range(1, 1+3):
diff --git a/fail2ban-restore-before-filter.patch b/fail2ban-restore-before-filter.patch
index e02a051..19e63f1 100644
--- a/fail2ban-restore-before-filter.patch
+++ b/fail2ban-restore-before-filter.patch
@@ -1,6 +1,6 @@
 diff -ru a/fail2ban/server/jail.py b/fail2ban/server/jail.py
---- a/fail2ban/server/jail.py	2026-07-22 15:57:18.082217451 +0200
-+++ b/fail2ban/server/jail.py	2026-07-22 15:57:18.082737114 +0200
+--- a/fail2ban/server/jail.py	2026-07-22 16:52:08.689675990 +0200
++++ b/fail2ban/server/jail.py	2026-07-22 16:52:08.691459983 +0200
 @@ -29,6 +29,7 @@
  import queue
  
@@ -37,3 +37,74 @@ diff -ru a/fail2ban/server/jail.py b/fail2ban/server/jail.py
  
  	def stop(self, stop=True, join=True):
  		"""Stop the jail, by stopping filter and actions threads.
+diff -ru a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py
+--- a/fail2ban/tests/servertestcase.py	2026-07-22 16:52:08.689876762 +0200
++++ b/fail2ban/tests/servertestcase.py	2026-07-22 16:52:08.691204309 +0200
+@@ -31,6 +31,7 @@
+ import re
+ import sys
+ import platform
++import threading
+ 
+ from ..server.failregex import Regex, FailRegex, RegexException
+ from ..server import actions as _actions
+@@ -38,7 +39,7 @@
+ from ..server.ipdns import DNSUtils, IPAddr
+ from ..server.jail import Jail
+ from ..server.jailthread import JailThread
+-from ..server.ticket import BanTicket
++from ..server.ticket import BanTicket, FailTicket
+ from ..server.utils import Utils
+ from .dummyjail import DummyJail
+ from .utils import LogCaptureTestCase, with_alt_time, MyTime
+@@ -1035,6 +1036,50 @@
+ 		jail = Jail(longname)
+ 		self.assertEqual(jail.name, longname)
+ 
++	def _stubStartJail(self, name):
++		# real Jail.start() control flow, stubs at the collaborator boundary
++		jail = Jail(name, backend="polling")
++		jail.restoreCurrentBans = lambda: None
++		class StubActions(object):
++			active = True
++			def start(self):
++				pass
++		class StubFilter(object):
++			started = False
++			ticketsAtStart = None
++			def __init__(self, jail):
++				self._jail = jail
++			def start(self):
++				self.started = True
++				self.ticketsAtStart = self._jail.hasFailTickets
++		jail._Jail__actions = StubActions()
++		jail._Jail__filter = StubFilter(jail)
++		for i in range(10):
++			jail.putFailTicket(FailTicket("192.0.2.%d" % i, time.time()))
++		return jail
++
++	def testStartWaitsForRestoredBansDrain(self):
++		# the filter starts only after the actions thread drained the
++		# restored-ban queue - otherwise its CPU-bound matching starves
++		# the banning thread of the GIL right after restart
++		jail = self._stubStartJail("j-drain")
++		def drain():
++			while jail.getFailTicket():
++				pass
++		threading.Timer(0.2, drain).start()
++		jail.start()
++		self.assertTrue(jail.filter.started)
++		self.assertFalse(jail.filter.ticketsAtStart)
++
++	def testStartAbortedIfStoppedWhileRestoring(self):
++		# a jail stopped during the drain wait (e.g. SIGTERM ran quit()
++		# reentrantly) must not start a filter thread post factum
++		jail = self._stubStartJail("j-abort")
++		threading.Timer(0.2,
++			lambda: setattr(jail.actions, "active", False)).start()
++		jail.start()
++		self.assertFalse(jail.filter.started)
++
+ 
+ class RegexTests(unittest.TestCase):
+ 
diff --git a/fail2ban-restore-idle-gate.patch b/fail2ban-restore-idle-gate.patch
index c1d22ba..f626b32 100644
--- a/fail2ban-restore-idle-gate.patch
+++ b/fail2ban-restore-idle-gate.patch
@@ -1,6 +1,6 @@
 diff -ru a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py
---- a/fail2ban/server/filterpyinotify.py	2026-07-22 16:02:09.905136616 +0200
-+++ b/fail2ban/server/filterpyinotify.py	2026-07-22 16:02:09.905862427 +0200
+--- a/fail2ban/server/filterpyinotify.py	2026-07-22 16:51:43.830456953 +0200
++++ b/fail2ban/server/filterpyinotify.py	2026-07-22 16:51:43.831988666 +0200
 @@ -122,8 +122,12 @@
  		if isWF and not os.path.isfile(path):
  			self._addPending(path, event)
@@ -17,8 +17,8 @@ diff -ru a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.
  		# be sure we process a file:
  		if not isWF:
 diff -ru a/fail2ban/server/server.py b/fail2ban/server/server.py
---- a/fail2ban/server/server.py	2026-07-22 16:02:09.904922968 +0200
-+++ b/fail2ban/server/server.py	2026-07-22 16:02:09.905735774 +0200
+--- a/fail2ban/server/server.py	2026-07-22 16:51:43.830235027 +0200
++++ b/fail2ban/server/server.py	2026-07-22 16:51:43.831868324 +0200
 @@ -267,7 +267,19 @@
  		with self.__lock:
  			jail = self.__jails[name]
@@ -40,3 +40,74 @@ diff -ru a/fail2ban/server/server.py b/fail2ban/server/server.py
  			elif name in self.__reload_state:
  				logSys.info("Jail %r reloaded", name)
  				del self.__reload_state[name]
+diff -ru a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py
+--- a/fail2ban/tests/filtertestcase.py	2026-07-22 16:51:43.830862689 +0200
++++ b/fail2ban/tests/filtertestcase.py	2026-07-22 16:51:43.832575891 +0200
+@@ -1113,6 +1113,22 @@
+ 			#time.sleep(0.2)			  # Give FS time to ack the removal
+ 			super(MonitorFailures, self).tearDown()
+ 
++		def test_idle_queues_inotify_events(self):
++			# a paused filter still consumes inotify events; they must land
++			# in the pending queue for _checkPending instead of being lost
++			# until the next write to the file
++			if self.filter.__class__.__name__ != 'FilterPyinotify':
++				raise unittest.SkipTest('FilterPyinotify only')
++			import pyinotify
++			self.filter.idle = True
++			try:
++				self.filter.callback(pyinotify.Event({
++					"mask": pyinotify.IN_MODIFY, "maskname": "IN_MODIFY",
++					"pathname": self.name}))
++				self.assertIn(self.name, self.filter.getPendingPaths())
++			finally:
++				self.filter.idle = False
++
+ 		def _sleep_4_poll(self):
+ 			# Since FilterPoll relies on time stamps and some
+ 			# actions might be happening too fast in the tests,
+diff -ru a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py
+--- a/fail2ban/tests/servertestcase.py	2026-07-22 16:51:43.830648374 +0200
++++ b/fail2ban/tests/servertestcase.py	2026-07-22 16:51:43.832357274 +0200
+@@ -1029,6 +1029,41 @@
+ 
+ class JailTests(unittest.TestCase):
+ 
++	def testStartJailPausesOtherFilters(self):
++		# while a jail starts (and restores its bans) the other alive
++		# jails' filters are paused; user-set idle stays untouched and
++		# everything is unpaused even when the start fails
++		srv = Server()
++		class StubFilter(object):
++			def __init__(self):
++				self.idle = False
++		class StubJail(object):
++			idle = False
++			def __init__(self, alive):
++				self._alive = alive
++				self.filter = StubFilter()
++			def isAlive(self):
++				return self._alive
++		running = StubJail(True)
++		useridle = StubJail(True)
++		useridle.filter.idle = True
++		seen = []
++		class StartingJail(StubJail):
++			def start(self):
++				seen.append((running.filter.idle, useridle.filter.idle))
++		class FailingJail(StubJail):
++			def start(self):
++				raise RuntimeError("start failed")
++		srv._Server__jails = {"running": running, "useridle": useridle,
++			"new": StartingJail(False), "bad": FailingJail(False)}
++		srv.startJail("new")
++		self.assertEqual(seen, [(True, True)])
++		self.assertFalse(running.filter.idle)
++		self.assertTrue(useridle.filter.idle)
++		self.assertRaises(RuntimeError, srv.startJail, "bad")
++		self.assertFalse(running.filter.idle)
++		self.assertTrue(useridle.filter.idle)
++
+ 	def testLongName(self):
+ 		# Just a smoke test for now
+ 		longname = "veryveryverylongname"
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/fail2ban.git/commitdiff/74db76d97f2db8dcde983b6ec2568841c1aacf22



More information about the pld-cvs-commit mailing list