diff --git a/config/action.d/dummy.conf b/config/action.d/dummy.conf index dc4e1dbf..41250c27 100644 --- a/config/action.d/dummy.conf +++ b/config/action.d/dummy.conf @@ -10,14 +10,23 @@ # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -actionstart = touch /var/run/fail2ban/fail2ban.dummy - printf %%b "\n" >> /var/run/fail2ban/fail2ban.dummy +actionstart = if [ ! -z '' ]; then touch ; fi; + printf %%b "\n" + echo "%(debug)s started" + +# Option: actionflush +# Notes.: command executed once to flush (clear) all IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = printf %%b "-*\n" + echo "%(debug)s clear all" # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # -actionstop = rm -f /var/run/fail2ban/fail2ban.dummy +actionstop = if [ ! -z '' ]; then rm -f ; fi; + echo "%(debug)s stopped" # Option: actioncheck # Notes.: command executed once before each actionban command @@ -31,7 +40,8 @@ actioncheck = # Tags: See jail.conf(5) man page # Values: CMD # -actionban = printf %%b "+\n" >> /var/run/fail2ban/fail2ban.dummy +actionban = printf %%b "+\n" + echo "%(debug)s banned (family: )" # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the @@ -39,9 +49,15 @@ actionban = printf %%b "+\n" >> /var/run/fail2ban/fail2ban.dummy # Tags: See jail.conf(5) man page # Values: CMD # -actionunban = printf %%b "-\n" >> /var/run/fail2ban/fail2ban.dummy +actionunban = printf %%b "-\n" + echo "%(debug)s unbanned (family: )" + + +debug = [] -- [Init] init = 123 +target = /var/run/fail2ban/fail2ban.dummy +to_target = >> diff --git a/config/action.d/iptables-allports.conf b/config/action.d/iptables-allports.conf index 15f3cbcc..dbea5984 100644 --- a/config/action.d/iptables-allports.conf +++ b/config/action.d/iptables-allports.conf @@ -26,7 +26,7 @@ actionstart = -N f2b- # Values: CMD # actionstop = -D -p -j f2b- - -F f2b- + -X f2b- # Option: actioncheck diff --git a/config/action.d/iptables-common.conf b/config/action.d/iptables-common.conf index a3921021..e016ef2f 100644 --- a/config/action.d/iptables-common.conf +++ b/config/action.d/iptables-common.conf @@ -16,6 +16,14 @@ after = iptables-blocktype.local iptables-common.local # iptables-blocktype.local is obsolete +[Definition] + +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = -F f2b- + [Init] diff --git a/config/action.d/iptables-ipset-proto4.conf b/config/action.d/iptables-ipset-proto4.conf index 2f63cd4b..30353f36 100644 --- a/config/action.d/iptables-ipset-proto4.conf +++ b/config/action.d/iptables-ipset-proto4.conf @@ -30,12 +30,19 @@ before = iptables-common.conf actionstart = ipset --create f2b- iphash -I -p -m multiport --dports -m set --match-set f2b- src -j + +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = ipset --flush f2b- + # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # actionstop = -D -p -m multiport --dports -m set --match-set f2b- src -j - ipset --flush f2b- + ipset --destroy f2b- # Option: actionban diff --git a/config/action.d/iptables-ipset-proto6-allports.conf b/config/action.d/iptables-ipset-proto6-allports.conf index 113f599e..b761ad8c 100644 --- a/config/action.d/iptables-ipset-proto6-allports.conf +++ b/config/action.d/iptables-ipset-proto6-allports.conf @@ -29,12 +29,18 @@ before = iptables-common.conf actionstart = ipset create hash:ip timeout -I -m set --match-set src -j +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = ipset flush + # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # actionstop = -D -m set --match-set src -j - ipset flush + ipset destroy # Option: actionban diff --git a/config/action.d/iptables-ipset-proto6.conf b/config/action.d/iptables-ipset-proto6.conf index dee7b029..e337eedf 100644 --- a/config/action.d/iptables-ipset-proto6.conf +++ b/config/action.d/iptables-ipset-proto6.conf @@ -29,12 +29,18 @@ before = iptables-common.conf actionstart = ipset create hash:ip timeout -I -p -m multiport --dports -m set --match-set src -j +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = ipset flush + # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # actionstop = -D -p -m multiport --dports -m set --match-set src -j - ipset flush + ipset destroy # Option: actionban diff --git a/config/action.d/iptables-multiport-log.conf b/config/action.d/iptables-multiport-log.conf index 1777ce62..62c2b4b1 100644 --- a/config/action.d/iptables-multiport-log.conf +++ b/config/action.d/iptables-multiport-log.conf @@ -26,13 +26,19 @@ actionstart = -N f2b- -I f2b--log -j LOG --log-prefix "$(expr f2b- : '\(.\{1,23\}\)'):DROP " --log-level warning -m limit --limit 6/m --limit-burst 2 -A f2b--log -j +# Option: actionflush +# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action) +# Values: CMD +# +actionflush = -F f2b- + -F f2b--log + # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # actionstop = -D -p -m multiport --dports -j f2b- - -F f2b- - -F f2b--log + -X f2b- -X f2b--log diff --git a/config/action.d/iptables-multiport.conf b/config/action.d/iptables-multiport.conf index 9fd87d20..c05f6ffc 100644 --- a/config/action.d/iptables-multiport.conf +++ b/config/action.d/iptables-multiport.conf @@ -23,7 +23,7 @@ actionstart = -N f2b- # Values: CMD # actionstop = -D -p -m multiport --dports -j f2b- - -F f2b- + -X f2b- # Option: actioncheck diff --git a/config/action.d/iptables-new.conf b/config/action.d/iptables-new.conf index 795bc601..5b316807 100644 --- a/config/action.d/iptables-new.conf +++ b/config/action.d/iptables-new.conf @@ -25,7 +25,7 @@ actionstart = -N f2b- # Values: CMD # actionstop = -D -m state --state NEW -p --dport -j f2b- - -F f2b- + -X f2b- # Option: actioncheck diff --git a/config/action.d/iptables-xt_recent-echo.conf b/config/action.d/iptables-xt_recent-echo.conf index 018d2cf6..1970de14 100644 --- a/config/action.d/iptables-xt_recent-echo.conf +++ b/config/action.d/iptables-xt_recent-echo.conf @@ -35,6 +35,12 @@ before = iptables-common.conf # shorter of the two timeouts actually matters. actionstart = if [ `id -u` -eq 0 ];then -I -m recent --update --seconds 3600 --name -j ;fi +# Option: actionflush +# +# [TODO] Flushing is currently not implemented for xt_recent +# +actionflush = + # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD diff --git a/config/action.d/iptables.conf b/config/action.d/iptables.conf index 38985ffa..bf83e24a 100644 --- a/config/action.d/iptables.conf +++ b/config/action.d/iptables.conf @@ -23,7 +23,7 @@ actionstart = -N f2b- # Values: CMD # actionstop = -D -p --dport -j f2b- - -F f2b- + -X f2b- # Option: actioncheck diff --git a/fail2ban/client/actionreader.py b/fail2ban/client/actionreader.py index b85f22a0..ace0b898 100644 --- a/fail2ban/client/actionreader.py +++ b/fail2ban/client/actionreader.py @@ -40,6 +40,7 @@ class ActionReader(DefinitionInitConfigReader): "actionstart": ["string", None], "actionstart_on_demand": ["string", None], "actionstop": ["string", None], + "actionflush": ["string", None], "actionreload": ["string", None], "actioncheck": ["string", None], "actionrepair": ["string", None], diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 9d518cfd..d00458ba 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -281,6 +281,8 @@ class CommandAction(ActionBase): self.actioncheck = '' ## Command executed in order to restore sane environment in error case. self.actionrepair = '' + ## Command executed in order to flush all bans at once (e. g. by stop/shutdown the system). + self.actionflush = '' ## Command executed in order to stop the system. self.actionstop = '' ## Command executed in case of reloading action. @@ -447,6 +449,25 @@ class CommandAction(ActionBase): if not self._processCmd('', aInfo): raise RuntimeError("Error unbanning %(ip)s" % aInfo) + def flush(self): + """Executes the "actionflush" command. + + Command executed in order to flush all bans at once (e. g. by stop/shutdown + the system), instead of unbunning of each single ticket. + + Replaces the tags in the action command with actions properties + and executes the resulting command. + """ + family = [] + # cumulate started families, if started on demand (conditional): + if self._startOnDemand: + for f in CommandAction.COND_FAMILIES: + if self.__started.get(f) == 1: # only real started: + family.append(f) + # if no started (on demand) actions: + if not family: return True + return self._executeOperation('', 'flushing', family=family) + def stop(self): """Executes the "actionstop" command. diff --git a/fail2ban/server/actions.py b/fail2ban/server/actions.py index 3a85e569..c33359c9 100644 --- a/fail2ban/server/actions.py +++ b/fail2ban/server/actions.py @@ -447,25 +447,37 @@ class Actions(JailThread, Mapping): If actions specified, don't flush list - just execute unban for given actions (reload, obsolete resp. removed actions). """ + log = True if actions is None: logSys.debug("Flush ban list") lst = self.__banManager.flushBanList() else: + log = False # don't log "[jail] Unban ..." if removing actions only. lst = iter(self.__banManager) cnt = 0 + # first we'll execute flush for actions supporting this operation: + unbactions = {} + for name, action in (actions if actions is not None else self._actions).iteritems(): + if hasattr(action, 'flush') and action.actionflush: + logSys.notice("[%s] Flush ticket(s) with %s", self._jail.name, name) + action.flush() + else: + unbactions[name] = action + actions = unbactions + # unban each ticket with non-flasheable actions: for ticket in lst: # delete ip from database also: if db and self._jail.database is not None: ip = str(ticket.getIP()) self._jail.database.delBan(self._jail, ip) # unban ip: - self.__unBan(ticket, actions=actions) + self.__unBan(ticket, actions=actions, log=log) cnt += 1 logSys.debug("Unbanned %s, %s ticket(s) in %r", cnt, self.__banManager.size(), self._jail.name) return cnt - def __unBan(self, ticket, actions=None): + def __unBan(self, ticket, actions=None, log=True): """Unbans host corresponding to the ticket. Executes the actions in order to unban the host given in the @@ -482,7 +494,7 @@ class Actions(JailThread, Mapping): unbactions = actions ip = ticket.getIP() aInfo = self.__getActionInfo(ticket) - if actions is None: + if log: logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"]) for name, action in unbactions.iteritems(): try: diff --git a/fail2ban/tests/fail2banclienttestcase.py b/fail2ban/tests/fail2banclienttestcase.py index 35e00421..3e046042 100644 --- a/fail2ban/tests/fail2banclienttestcase.py +++ b/fail2ban/tests/fail2banclienttestcase.py @@ -762,6 +762,7 @@ class Fail2banServerTest(Fail2banClientServerBase): "norestored = %(_exec_once)s", "restore = ", "info = ", + "_use_flush_ = echo [] : -- flushing IPs", "actionstart = echo '[%(name)s] %(actname)s: ** start'", start, "actionreload = echo '[%(name)s] %(actname)s: .. reload'", reload, "actionban = echo '[%(name)s] %(actname)s: ++ ban %(restore)s%(info)s'", ban, @@ -788,7 +789,8 @@ class Fail2banServerTest(Fail2banClientServerBase): if 1 in actions else "", " test-action2[name='%(__name__)s', restore='restored: ', info=', err-code: ']" \ if 2 in actions else "", - " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ + " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: '," + " actionflush=<_use_flush_>]" \ if 3 in actions else "", "logpath = " + test1log, " " + test2log if 2 in enabled else "", @@ -802,7 +804,8 @@ class Fail2banServerTest(Fail2banClientServerBase): "action = ", " test-action2[name='%(__name__)s', restore='restored: ', info=', err-code: ']" \ if 2 in actions else "", - " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" \ + " test-action2[name='%(__name__)s', actname=test-action3, _exec_once=1, restore='restored: ']" + " actionflush=<_use_flush_>]" \ if 3 in actions else "", "logpath = " + test2log, "enabled = true" if 2 in enabled else "", @@ -874,6 +877,12 @@ class Fail2banServerTest(Fail2banClientServerBase): self.assertLogged( "Creating new jail 'test-jail2'", "Jail 'test-jail2' started", all=True) + # test action3 removed, test flushing successful (and no single unban occurred): + self.assertLogged( + "stdout: '[test-jail1] test-action3: -- flushing IPs'", + "stdout: '[test-jail1] test-action3: __ stop'", all=True) + self.assertNotLogged( + "stdout: '[test-jail1] test-action3: -- unban 192.0.2.1'") # update action1, delete action2 (should be stopped via configuration)... self.pruneLog("[test-phase 2a]") @@ -969,12 +978,18 @@ class Fail2banServerTest(Fail2banClientServerBase): "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.8 restored: 1'", all=True) - # don't need actions anymore: - _write_action_cfg(actname="test-action2", allow=False) - _write_jail_cfg(actions=[]) + # ban manually to test later flush by unban all: + self.pruneLog("[test-phase 2d]") + self.execSuccess(startparams, + "set", "test-jail2", "banip", "192.0.2.21") + self.execSuccess(startparams, + "set", "test-jail2", "banip", "192.0.2.22") + self.assertLogged( + "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22", + "stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22 ", all=True, wait=MID_WAITTIME) # restart jail with unban all: - self.pruneLog("[test-phase 2d]") + self.pruneLog("[test-phase 2e]") self.execSuccess(startparams, "restart", "--unban", "test-jail2") self.assertLogged( @@ -986,12 +1001,26 @@ class Fail2banServerTest(Fail2banClientServerBase): "[test-jail2] Unban 192.0.2.4", "[test-jail2] Unban 192.0.2.8", all=True ) + # test unban (action2): + self.assertLogged( + "stdout: '[test-jail2] test-action2: -- unban 192.0.2.21", + "stdout: '[test-jail2] test-action2: -- unban 192.0.2.22'", all=True) + # test flush (action3, and no single unban via action3 occurred): + self.assertLogged( + "stdout: '[test-jail2] test-action3: -- flushing IPs'") + self.assertNotLogged( + "stdout: '[test-jail2] test-action3: -- unban 192.0.2.21'", + "stdout: '[test-jail2] test-action3: -- unban 192.0.2.22'", all=True) # no more ban (unbanned all): self.assertNotLogged( "[test-jail2] Ban 192.0.2.4", "[test-jail2] Ban 192.0.2.8", all=True ) + # don't need actions anymore: + _write_action_cfg(actname="test-action2", allow=False) + _write_jail_cfg(actions=[]) + # reload jail1 without restart (without ban/unban): self.pruneLog("[test-phase 3]") self.execSuccess(startparams, "reload", "test-jail1") diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 060d8e40..1644d895 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -1182,6 +1182,33 @@ class ServerConfigReaderTests(LogCaptureTestCase): # 'start', 'stop' - should be found (logged) on action start/stop, # etc. testJailsActions = ( + # dummy -- + ('j-dummy', 'dummy[name=%(__name__)s, init="==", target="/tmp/fail2ban.dummy"]', { + 'ip4': ('family: inet4',), 'ip6': ('family: inet6',), + 'start': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- started"`', + ), + 'flush': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- clear all"`', + ), + 'stop': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- stopped"`', + ), + 'ip4-check': (), + 'ip6-check': (), + 'ip4-ban': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- banned 192.0.2.1 (family: inet4)"`', + ), + 'ip4-unban': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- unbanned 192.0.2.1 (family: inet4)"`', + ), + 'ip6-ban': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- banned 2001:db8:: (family: inet6)"`', + ), + 'ip6-unban': ( + '`echo "[j-dummy] dummy /tmp/fail2ban.dummy -- unbanned 2001:db8:: (family: inet6)"`', + ), + }), # iptables-multiport -- ('j-w-iptables-mp', 'iptables-multiport[name=%(__name__)s, bantime="10m", port="http,https", protocol="tcp", chain="INPUT"]', { 'ip4': ('`iptables ', 'icmp-port-unreachable'), 'ip6': ('`ip6tables ', 'icmp6-port-unreachable'), @@ -1195,6 +1222,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ip6tables -w -A f2b-j-w-iptables-mp -j RETURN`", "`ip6tables -w -I INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", ), + 'flush': ( + "`iptables -w -F f2b-j-w-iptables-mp`", + "`ip6tables -w -F f2b-j-w-iptables-mp`", + ), 'stop': ( "`iptables -w -D INPUT -p tcp -m multiport --dports http,https -j f2b-j-w-iptables-mp`", "`iptables -w -F f2b-j-w-iptables-mp`", @@ -1235,6 +1266,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ip6tables -w -A f2b-j-w-iptables-ap -j RETURN`", "`ip6tables -w -I INPUT -p tcp -j f2b-j-w-iptables-ap`", ), + 'flush': ( + "`iptables -w -F f2b-j-w-iptables-ap`", + "`ip6tables -w -F f2b-j-w-iptables-ap`", + ), 'stop': ( "`iptables -w -D INPUT -p tcp -j f2b-j-w-iptables-ap`", "`iptables -w -F f2b-j-w-iptables-ap`", @@ -1273,6 +1308,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ipset create f2b-j-w-iptables-ipset6 hash:ip timeout 600 family inet6`", "`ip6tables -w -I INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset6 src -j REJECT --reject-with icmp6-port-unreachable`", ), + 'flush': ( + "`ipset flush f2b-j-w-iptables-ipset`", + "`ipset flush f2b-j-w-iptables-ipset6`", + ), 'stop': ( "`iptables -w -D INPUT -p tcp -m multiport --dports http -m set --match-set f2b-j-w-iptables-ipset src -j REJECT --reject-with icmp-port-unreachable`", "`ipset flush f2b-j-w-iptables-ipset`", @@ -1307,6 +1346,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ipset create f2b-j-w-iptables-ipset-ap6 hash:ip timeout 600 family inet6`", "`ip6tables -w -I INPUT -m set --match-set f2b-j-w-iptables-ipset-ap6 src -j REJECT --reject-with icmp6-port-unreachable`", ), + 'flush': ( + "`ipset flush f2b-j-w-iptables-ipset-ap`", + "`ipset flush f2b-j-w-iptables-ipset-ap6`", + ), 'stop': ( "`iptables -w -D INPUT -m set --match-set f2b-j-w-iptables-ipset-ap src -j REJECT --reject-with icmp-port-unreachable`", "`ipset flush f2b-j-w-iptables-ipset-ap`", @@ -1343,6 +1386,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ip6tables -w -A f2b-j-w-iptables -j RETURN`", "`ip6tables -w -I INPUT -p tcp --dport http -j f2b-j-w-iptables`", ), + 'flush': ( + "`iptables -w -F f2b-j-w-iptables`", + "`ip6tables -w -F f2b-j-w-iptables`", + ), 'stop': ( "`iptables -w -D INPUT -p tcp --dport http -j f2b-j-w-iptables`", "`iptables -w -F f2b-j-w-iptables`", @@ -1383,6 +1430,10 @@ class ServerConfigReaderTests(LogCaptureTestCase): "`ip6tables -w -A f2b-j-w-iptables-new -j RETURN`", "`ip6tables -w -I INPUT -m state --state NEW -p tcp --dport http -j f2b-j-w-iptables-new`", ), + 'flush': ( + "`iptables -w -F f2b-j-w-iptables-new`", + "`ip6tables -w -F f2b-j-w-iptables-new`", + ), 'stop': ( "`iptables -w -D INPUT -m state --state NEW -p tcp --dport http -j f2b-j-w-iptables-new`", "`iptables -w -F f2b-j-w-iptables-new`", @@ -1684,6 +1735,11 @@ class ServerConfigReaderTests(LogCaptureTestCase): action.unban(ainfo['ip6']) self.assertLogged(*tests['ip6-check']+tests['ip6-unban'], all=True) self.assertNotLogged(*tests['ip4'], all=True) + # test flush for actions should supported this: + if tests.get('flush'): + self.pruneLog('# === flush ===') + action.flush() + self.assertLogged(*tests['flush'], all=True) # test stop : self.pruneLog('# === stop ===') action.stop()