[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