From 504e5ba6f22e9e0bc5121787e1ef080d2ca98c83 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 10 May 2016 18:40:07 +0200 Subject: [PATCH] actions support IPv6 now: - introduced "conditional" sections, see for example `[Init?family=inet6]`; - iptables-common and other iptables config(s) made IPv6 capable; - several small code optimizations; * all test cases passed (py3.x compatible); --- config/action.d/iptables-common.conf | 20 +++++ .../iptables-ipset-proto6-allports.conf | 26 ++++-- config/action.d/iptables-ipset-proto6.conf | 26 ++++-- config/action.d/iptables-xt_recent-echo.conf | 24 +++-- fail2ban/client/configparserinc.py | 46 ++++++---- fail2ban/server/action.py | 88 +++++++++++++------ fail2ban/tests/actiontestcase.py | 16 ++-- fail2ban/tests/filtertestcase.py | 2 - 8 files changed, 176 insertions(+), 72 deletions(-) diff --git a/config/action.d/iptables-common.conf b/config/action.d/iptables-common.conf index 45819e3d..a3921021 100644 --- a/config/action.d/iptables-common.conf +++ b/config/action.d/iptables-common.conf @@ -6,6 +6,9 @@ # used in all iptables based actions by default. # # The user can override the defaults in iptables-common.local +# +# Modified: Alexander Koeppe , Serg G. Brester +# made config file IPv6 capable (see new section Init?family=inet6) [INCLUDES] @@ -13,6 +16,7 @@ after = iptables-blocktype.local iptables-common.local # iptables-blocktype.local is obsolete + [Init] # Option: chain @@ -62,3 +66,19 @@ lockingopt = -w # Notes.: Actual command to be executed, including common to all calls options # Values: STRING iptables = iptables + + +[Init?family=inet6] + +# Option: blocktype (ipv6) +# Note: This is what the action does with rules. This can be any jump target +# as per the iptables man page (section 8). Common values are DROP +# REJECT, REJECT --reject-with icmp6-port-unreachable +# Values: STRING +blocktype = REJECT --reject-with icmp6-port-unreachable + +# Option: iptables (ipv6) +# Notes.: Actual command to be executed, including common to all calls options +# Values: STRING +iptables = ip6tables + diff --git a/config/action.d/iptables-ipset-proto6-allports.conf b/config/action.d/iptables-ipset-proto6-allports.conf index 1f1d336f..113f599e 100644 --- a/config/action.d/iptables-ipset-proto6-allports.conf +++ b/config/action.d/iptables-ipset-proto6-allports.conf @@ -12,6 +12,9 @@ # # If you are running on an older kernel you make need to patch in external # modules which probably won't be protocol version 6. +# +# Modified: Alexander Koeppe , Serg G. Brester +# made config file IPv6 capable (see new section Init?family=inet6) [INCLUDES] @@ -23,16 +26,16 @@ before = iptables-common.conf # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -actionstart = ipset create f2b- hash:ip timeout - -I -m set --match-set f2b- src -j +actionstart = ipset create hash:ip timeout + -I -m set --match-set src -j # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # -actionstop = -D -m set --match-set f2b- src -j - ipset flush f2b- - ipset destroy f2b- +actionstop = -D -m set --match-set src -j + ipset flush + ipset destroy # Option: actionban # Notes.: command executed when banning an IP. Take care that the @@ -40,7 +43,7 @@ actionstop = -D -m set --match-set f2b- src -j timeout -exist +actionban = ipset add timeout -exist # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the @@ -48,7 +51,7 @@ actionban = ipset add f2b- timeout -exist # Tags: See jail.conf(5) man page # Values: CMD # -actionunban = ipset del f2b- -exist +actionunban = ipset del -exist [Init] @@ -57,3 +60,12 @@ actionunban = ipset del f2b- -exist # Values: [ NUM ] Default: 600 # bantime = 600 + +ipmset = f2b- +familyopt = + + +[Init?family=inet6] + +ipmset = f2b-6 +familyopt = family inet6 diff --git a/config/action.d/iptables-ipset-proto6.conf b/config/action.d/iptables-ipset-proto6.conf index 3b51ef58..dee7b029 100644 --- a/config/action.d/iptables-ipset-proto6.conf +++ b/config/action.d/iptables-ipset-proto6.conf @@ -12,6 +12,9 @@ # # If you are running on an older kernel you make need to patch in external # modules. +# +# Modified: Alexander Koeppe , Serg G. Brester +# made config file IPv6 capable (see new section Init?family=inet6) [INCLUDES] @@ -23,16 +26,16 @@ before = iptables-common.conf # Notes.: command executed once at the start of Fail2Ban. # Values: CMD # -actionstart = ipset create f2b- hash:ip timeout - -I -p -m multiport --dports -m set --match-set f2b- src -j +actionstart = ipset create hash:ip timeout + -I -p -m multiport --dports -m set --match-set src -j # 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- +actionstop = -D -p -m multiport --dports -m set --match-set src -j + ipset flush + ipset destroy # Option: actionban # Notes.: command executed when banning an IP. Take care that the @@ -40,7 +43,7 @@ actionstop = -D -p -m multiport --dports -m # Tags: See jail.conf(5) man page # Values: CMD # -actionban = ipset add f2b- timeout -exist +actionban = ipset add timeout -exist # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the @@ -48,7 +51,7 @@ actionban = ipset add f2b- timeout -exist # Tags: See jail.conf(5) man page # Values: CMD # -actionunban = ipset del f2b- -exist +actionunban = ipset del -exist [Init] @@ -57,3 +60,12 @@ actionunban = ipset del f2b- -exist # Values: [ NUM ] Default: 600 # bantime = 600 + +ipmset = f2b- +familyopt = + + +[Init?family=inet6] + +ipmset = f2b-6 +familyopt = family inet6 diff --git a/config/action.d/iptables-xt_recent-echo.conf b/config/action.d/iptables-xt_recent-echo.conf index d3d43f86..018d2cf6 100644 --- a/config/action.d/iptables-xt_recent-echo.conf +++ b/config/action.d/iptables-xt_recent-echo.conf @@ -2,7 +2,8 @@ # # Author: Zbigniew Jędrzejewski-Szmek # -# +# Modified: Alexander Koeppe , Serg G. Brester +# made config file IPv6 capable [INCLUDES] @@ -22,30 +23,30 @@ before = iptables-common.conf # iptables-persistent package). # # Explanation of the rule below: -# Check if any packets coming from an IP on the f2b- +# Check if any packets coming from an IP on the # list have been seen in the last 3600 seconds. If yes, update the # timestamp for this IP and drop the packet. If not, let the packet # through. # -# Fail2ban inserts blacklisted hosts into the f2b- list +# Fail2ban inserts blacklisted hosts into the list # and removes them from the list after some time, according to its # own rules. The 3600 second timeout is independent and acts as a # safeguard in case the fail2ban process dies unexpectedly. The # shorter of the two timeouts actually matters. -actionstart = if [ `id -u` -eq 0 ];then -I -m recent --update --seconds 3600 --name f2b- -j ;fi +actionstart = if [ `id -u` -eq 0 ];then -I -m recent --update --seconds 3600 --name -j ;fi # Option: actionstop # Notes.: command executed once at the end of Fail2Ban # Values: CMD # -actionstop = echo / > /proc/net/xt_recent/f2b- - if [ `id -u` -eq 0 ];then -D -m recent --update --seconds 3600 --name f2b- -j ;fi +actionstop = echo / > /proc/net/xt_recent/ + if [ `id -u` -eq 0 ];then -D -m recent --update --seconds 3600 --name -j ;fi # Option: actioncheck # Notes.: command executed once before each actionban command # Values: CMD # -actioncheck = test -e /proc/net/xt_recent/f2b- +actioncheck = test -e /proc/net/xt_recent/ # Option: actionban # Notes.: command executed when banning an IP. Take care that the @@ -53,7 +54,7 @@ actioncheck = test -e /proc/net/xt_recent/f2b- # Tags: See jail.conf(5) man page # Values: CMD # -actionban = echo + > /proc/net/xt_recent/f2b- +actionban = echo + > /proc/net/xt_recent/ # Option: actionunban # Notes.: command executed when unbanning an IP. Take care that the @@ -61,7 +62,12 @@ actionban = echo + > /proc/net/xt_recent/f2b- # Tags: See jail.conf(5) man page # Values: CMD # -actionunban = echo - > /proc/net/xt_recent/f2b- +actionunban = echo - > /proc/net/xt_recent/ [Init] +iptname = f2b- + +[Init?family=inet6] + +iptname = f2b-6 diff --git a/fail2ban/client/configparserinc.py b/fail2ban/client/configparserinc.py index f4975857..617ff8f6 100644 --- a/fail2ban/client/configparserinc.py +++ b/fail2ban/client/configparserinc.py @@ -25,6 +25,7 @@ __copyright__ = 'Copyright (c) 2007 Yaroslav Halchenko' __license__ = 'GPL' import os +import re import sys from ..helpers import getLogger @@ -99,6 +100,8 @@ after = 1.conf SECTION_NAME = "INCLUDES" + CONDITIONAL_RE = re.compile(r"^(\w+)(\?.+)$") + if sys.version_info >= (3,2): # overload constructor only for fancy new Python3's def __init__(self, share_config=None, *args, **kwargs): @@ -225,21 +228,31 @@ after = 1.conf # merge defaults and all sections to self: alld.update(cfg.get_defaults()) for n, s in cfg.get_sections().iteritems(): - if isinstance(s, dict): - s2 = alls.get(n) - if isinstance(s2, dict): - # save previous known values, for possible using in local interpolations later: - sk = {} - for k, v in s2.iteritems(): - if not k.startswith('known/') and k != '__name__': - sk['known/'+k] = v - s2.update(sk) - # merge section - s2.update(s) - else: - alls[n] = s.copy() + curalls = alls + # conditional sections + cond = SafeConfigParserWithIncludes.CONDITIONAL_RE.match(n) + if cond: + n, cond = cond.groups() + s = s.copy() + try: + del(s['__name__']) + except KeyError: + pass + for k in s.keys(): + v = s.pop(k) + s[k + cond] = v + s2 = alls.get(n) + if isinstance(s2, dict): + # save previous known values, for possible using in local interpolations later: + sk = {} + for k, v in s2.iteritems(): + if not k.startswith('known/') and k != '__name__': + sk['known/'+k] = v + s2.update(sk) + # merge section + s2.update(s) else: - alls[n] = s + alls[n] = s.copy() return ret @@ -254,9 +267,12 @@ after = 1.conf def merge_section(self, section, options, pref='known/'): alls = self.get_sections() + if pref == '': + alls[section].update(options) + return sk = {} for k, v in options.iteritems(): - if pref == '' or (not k.startswith(pref) and k != '__name__'): + if not k.startswith(pref) and k != '__name__': sk[pref+k] = v alls[section].update(sk) diff --git a/fail2ban/server/action.py b/fail2ban/server/action.py index 446ae853..cfa03403 100644 --- a/fail2ban/server/action.py +++ b/fail2ban/server/action.py @@ -32,6 +32,7 @@ import time from abc import ABCMeta from collections import MutableMapping +from .ipdns import asip from .utils import Utils from ..helpers import getLogger @@ -41,6 +42,11 @@ logSys = getLogger(__name__) # Create a lock for running system commands _cmd_lock = threading.Lock() +# Todo: make it configurable resp. automatically set, ex.: `[ -f /proc/net/if_inet6 ] && echo 'yes' || echo 'no'`: +allowed_ipv6 = True + +# compiled RE for tag name (replacement name) +TAG_CRE = re.compile(r'<([^ <>]+)>') class CallingMap(MutableMapping): """A Mapping type which returns the result of callable values. @@ -256,14 +262,18 @@ class CommandAction(ActionBase): Replace the tags in the action command with actions properties and executes the resulting command. """ - if (self._properties and - not self.substituteRecursiveTags(self._properties)): - self._logSys.error( - "properties contain self referencing definitions " - "and cannot be resolved") - raise RuntimeError("Error starting action") - startCmd = self.replaceTag(self.actionstart, self._properties) - if not self.executeCmd(startCmd, self.timeout): + # check valid tags in properties (raises ValueError if self recursion, etc.): + if self._properties: + self.substituteRecursiveTags(self._properties) + # common (resp. ipv4): + startCmd = self.replaceTag('', self._properties, conditional='family=inet4') + res = self.executeCmd(startCmd, self.timeout) + # start ipv6 actions if available: + if allowed_ipv6: + startCmd6 = self.replaceTag('', self._properties, conditional='family=inet6') + if startCmd6 != startCmd: + res &= self.executeCmd(startCmd6, self.timeout) + if not res: raise RuntimeError("Error starting action") @property @@ -289,7 +299,7 @@ class CommandAction(ActionBase): Dictionary which includes information in relation to the ban. """ - if not self._processCmd(self.actionban, aInfo): + if not self._processCmd('', aInfo): raise RuntimeError("Error banning %(ip)s" % aInfo) @property @@ -315,7 +325,7 @@ class CommandAction(ActionBase): Dictionary which includes information in relation to the ban. """ - if not self._processCmd(self.actionunban, aInfo): + if not self._processCmd('', aInfo): raise RuntimeError("Error unbanning %(ip)s" % aInfo) @property @@ -350,12 +360,19 @@ class CommandAction(ActionBase): Replaces the tags in the action command with actions properties and executes the resulting command. """ - stopCmd = self.replaceTag(self.actionstop, self._properties) - if not self.executeCmd(stopCmd, self.timeout): + # common (resp. ipv4): + stopCmd = self.replaceTag('', self._properties, conditional='family=inet4') + res = self.executeCmd(stopCmd, self.timeout) + # ipv6 actions if available: + if allowed_ipv6: + stopCmd6 = self.replaceTag('', self._properties, conditional='family=inet6') + if stopCmd6 != stopCmd: + res &= self.executeCmd(stopCmd6, self.timeout) + if not res: raise RuntimeError("Error stopping action") @classmethod - def substituteRecursiveTags(cls, tags): + def substituteRecursiveTags(cls, tags, conditional=''): """Sort out tag definitions within other tags. Since v.0.9.2 supports embedded interpolation (see test cases for examples). @@ -374,7 +391,7 @@ class CommandAction(ActionBase): Dictionary of tags(keys) and their values, with tags within the values recursively replaced. """ - t = re.compile(r'<([^ <>]+)>') + t = TAG_CRE # repeat substitution while embedded-recursive (repFlag is True) while True: repFlag = False @@ -394,14 +411,21 @@ class CommandAction(ActionBase): if found_tag == tag or found_tag in done: # recursive definitions are bad #logSys.log(5, 'recursion fail tag: %s value: %s' % (tag, value) ) - return False - if found_tag in cls._escapedTags or not found_tag in tags: + raise ValueError( + "properties contain self referencing definitions " + "and cannot be resolved, fail tag: %s value: %s" % (tag, value)) + repl = None + if found_tag not in cls._escapedTags: + repl = tags.get(found_tag + '?' + conditional) + if repl is None: + repl = tags.get(found_tag) + if repl is None: # Escaped or missing tags - just continue on searching after end of match # Missing tags are ok - cInfo can contain aInfo elements like and valid shell # constructs like . m = t.search(value, m.end()) continue - value = value.replace('<%s>' % found_tag , tags[found_tag]) + value = value.replace('<%s>' % found_tag, repl) #logSys.log(5, 'value now: %s' % value) done.append(found_tag) m = t.search(value, m.start()) @@ -443,7 +467,7 @@ class CommandAction(ActionBase): return value @classmethod - def replaceTag(cls, query, aInfo): + def replaceTag(cls, query, aInfo, conditional=''): """Replaces tags in `query` with property values. Parameters @@ -459,7 +483,7 @@ class CommandAction(ActionBase): `query` string with tags replaced. """ string = query - aInfo = cls.substituteRecursiveTags(aInfo) + aInfo = cls.substituteRecursiveTags(aInfo, conditional) for tag in aInfo: if "<%s>" % tag in query: value = str(aInfo[tag]) # assure string @@ -469,10 +493,10 @@ class CommandAction(ActionBase): value = cls.escapeTag(value) string = string.replace('<' + tag + '>', value) # New line - string = string.replace("
", '\n') + string = reduce(lambda s, kv: s.replace(*kv), (("
", '\n'), ("", " ")), string) return string - def _processCmd(self, cmd, aInfo = None): + def _processCmd(self, cmd, aInfo=None, conditional=''): """Executes a command with preliminary checks and substitutions. Before executing any commands, executes the "check" command first @@ -496,7 +520,17 @@ class CommandAction(ActionBase): self._logSys.debug("Nothing to do") return True - checkCmd = self.replaceTag(self.actioncheck, self._properties) + if conditional == '': + conditional = 'family=inet4' + if allowed_ipv6: + try: + ip = aInfo["ip"] + if ip and asip(ip).isIPv6: + conditional = 'family=inet6' + except KeyError: + pass + + checkCmd = self.replaceTag('', self._properties, conditional=conditional) if not self.executeCmd(checkCmd, self.timeout): self._logSys.error( "Invariant check failed. Trying to restore a sane environment") @@ -506,15 +540,15 @@ class CommandAction(ActionBase): self._logSys.critical("Unable to restore environment") return False + # Replace static fields + realCmd = self.replaceTag(cmd, self._properties, conditional=conditional) + # Replace tags - if not aInfo is None: - realCmd = self.replaceTag(cmd, aInfo) + if aInfo is not None: + realCmd = self.replaceTag(realCmd, aInfo, conditional=conditional) else: realCmd = cmd - # Replace static fields - realCmd = self.replaceTag(realCmd, self._properties) - return self.executeCmd(realCmd, self.timeout) @staticmethod diff --git a/fail2ban/tests/actiontestcase.py b/fail2ban/tests/actiontestcase.py index 1872eb1f..d0c1495a 100644 --- a/fail2ban/tests/actiontestcase.py +++ b/fail2ban/tests/actiontestcase.py @@ -54,12 +54,17 @@ class CommandActionTest(LogCaptureTestCase): 'xyz': "890 ", } # Recursion is bad - self.assertFalse(CommandAction.substituteRecursiveTags({'A': ''})) - self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': ''})) - self.assertFalse(CommandAction.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) + self.assertRaises(ValueError, + lambda: CommandAction.substituteRecursiveTags({'A': ''})) + self.assertRaises(ValueError, + lambda: CommandAction.substituteRecursiveTags({'A': '', 'B': ''})) + self.assertRaises(ValueError, + lambda: CommandAction.substituteRecursiveTags({'A': '', 'B': '', 'C': ''})) # Unresolveable substition - self.assertFalse(CommandAction.substituteRecursiveTags({'A': 'to= fromip=', 'C': '', 'B': '', 'D': ''})) - self.assertFalse(CommandAction.substituteRecursiveTags({'failregex': 'to= fromip=', 'sweet': '', 'honeypot': '', 'ignoreregex': ''})) + self.assertRaises(ValueError, + lambda: CommandAction.substituteRecursiveTags({'A': 'to= fromip=', 'C': '', 'B': '', 'D': ''})) + self.assertRaises(ValueError, + lambda: CommandAction.substituteRecursiveTags({'failregex': 'to= fromip=', 'sweet': '', 'honeypot': '', 'ignoreregex': ''})) # missing tags are ok self.assertEqual(CommandAction.substituteRecursiveTags({'A': ''}), {'A': ''}) self.assertEqual(CommandAction.substituteRecursiveTags({'A': ' ','X':'fun'}), {'A': ' fun', 'X':'fun'}) @@ -127,6 +132,7 @@ class CommandActionTest(LogCaptureTestCase): CallingMap(matches=lambda: str(10))), "09 10 11") + def testReplaceNoTag(self): # As tag not present, therefore callable should not be called # Will raise ValueError if it is self.assertEqual( diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index b5a9b8d5..44e06067 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -1416,7 +1416,6 @@ class DNSUtilsNetworkTests(unittest.TestCase): self.assertNotEqual(ip4[0], None) self.assertTrue(ip4[0] is not None) self.assertFalse(ip4[0] is None) - self.assertLess(None, ip4[0]) self.assertLess(ip4[0], ip4[1]) self.assertLess(ip4[1], ip4[2]) self.assertEqual(sorted(reversed(ip4)), ip4) @@ -1424,7 +1423,6 @@ class DNSUtilsNetworkTests(unittest.TestCase): self.assertNotEqual(ip6[0], None) self.assertTrue(ip6[0] is not None) self.assertFalse(ip6[0] is None) - self.assertLess(None, ip6[0]) self.assertLess(ip6[0], ip6[1]) self.assertLess(ip6[1], ip6[2]) self.assertEqual(sorted(reversed(ip6)), ip6)