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);
pull/1414/head
sebres 2016-05-10 18:40:07 +02:00
parent 75028585c0
commit 504e5ba6f2
8 changed files with 176 additions and 72 deletions

View File

@ -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 <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# 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 <lockingopt>
[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 <lockingopt>

View File

@ -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 <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# 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-<name> hash:ip timeout <bantime>
<iptables> -I <chain> -m set --match-set f2b-<name> src -j <blocktype>
actionstart = ipset create <ipmset> hash:ip timeout <bantime><familyopt>
<iptables> -I <chain> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = <iptables> -D <chain> -m set --match-set f2b-<name> src -j <blocktype>
ipset flush f2b-<name>
ipset destroy f2b-<name>
actionstop = <iptables> -D <chain> -m set --match-set <ipmset> src -j <blocktype>
ipset flush <ipmset>
ipset destroy <ipmset>
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
@ -40,7 +43,7 @@ actionstop = <iptables> -D <chain> -m set --match-set f2b-<name> src -j <blockty
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
@ -48,7 +51,7 @@ actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = ipset del f2b-<name> <ip> -exist
actionunban = ipset del <ipmset> <ip> -exist
[Init]
@ -57,3 +60,12 @@ actionunban = ipset del f2b-<name> <ip> -exist
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name>
familyopt =
[Init?family=inet6]
ipmset = f2b-<name>6
familyopt = <sp>family inet6

View File

@ -12,6 +12,9 @@
#
# If you are running on an older kernel you make need to patch in external
# modules.
#
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# 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-<name> hash:ip timeout <bantime>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
actionstart = ipset create <ipmset> hash:ip timeout <bantime><familyopt>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
ipset flush f2b-<name>
ipset destroy f2b-<name>
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
ipset flush <ipmset>
ipset destroy <ipmset>
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
@ -40,7 +43,7 @@ actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
@ -48,7 +51,7 @@ actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = ipset del f2b-<name> <ip> -exist
actionunban = ipset del <ipmset> <ip> -exist
[Init]
@ -57,3 +60,12 @@ actionunban = ipset del f2b-<name> <ip> -exist
# Values: [ NUM ] Default: 600
#
bantime = 600
ipmset = f2b-<name>
familyopt =
[Init?family=inet6]
ipmset = f2b-<name>6
familyopt = <sp>family inet6

View File

@ -2,7 +2,8 @@
#
# Author: Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
#
#
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# 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-<name>
# Check if any packets coming from an IP on the <iptname>
# 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-<name> list
# Fail2ban inserts blacklisted hosts into the <iptname> 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 <iptables> -I <chain> -m recent --update --seconds 3600 --name f2b-<name> -j <blocktype>;fi
actionstart = if [ `id -u` -eq 0 ];then <iptables> -I <chain> -m recent --update --seconds 3600 --name <iptname> -j <blocktype>;fi
# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = echo / > /proc/net/xt_recent/f2b-<name>
if [ `id -u` -eq 0 ];then <iptables> -D <chain> -m recent --update --seconds 3600 --name f2b-<name> -j <blocktype>;fi
actionstop = echo / > /proc/net/xt_recent/<iptname>
if [ `id -u` -eq 0 ];then <iptables> -D <chain> -m recent --update --seconds 3600 --name <iptname> -j <blocktype>;fi
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = test -e /proc/net/xt_recent/f2b-<name>
actioncheck = test -e /proc/net/xt_recent/<iptname>
# 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-<name>
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = echo +<ip> > /proc/net/xt_recent/f2b-<name>
actionban = echo +<ip> > /proc/net/xt_recent/<iptname>
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
@ -61,7 +62,12 @@ actionban = echo +<ip> > /proc/net/xt_recent/f2b-<name>
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = echo -<ip> > /proc/net/xt_recent/f2b-<name>
actionunban = echo -<ip> > /proc/net/xt_recent/<iptname>
[Init]
iptname = f2b-<name>
[Init?family=inet6]
iptname = f2b-<name>6

View File

@ -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)

View File

@ -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('<actionstart>', self._properties, conditional='family=inet4')
res = self.executeCmd(startCmd, self.timeout)
# start ipv6 actions if available:
if allowed_ipv6:
startCmd6 = self.replaceTag('<actionstart>', 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('<actionban>', 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('<actionunban>', 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('<actionstop>', self._properties, conditional='family=inet4')
res = self.executeCmd(stopCmd, self.timeout)
# ipv6 actions if available:
if allowed_ipv6:
stopCmd6 = self.replaceTag('<actionstop>', 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 <HOST> and valid shell
# constructs like <STDIN>.
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("<br>", '\n')
string = reduce(lambda s, kv: s.replace(*kv), (("<br>", '\n'), ("<sp>", " ")), 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('<actioncheck>', 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

View File

@ -54,12 +54,17 @@ class CommandActionTest(LogCaptureTestCase):
'xyz': "890 <ABC>",
}
# Recursion is bad
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<A>'}))
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
self.assertRaises(ValueError,
lambda: CommandAction.substituteRecursiveTags({'A': '<A>'}))
self.assertRaises(ValueError,
lambda: CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertRaises(ValueError,
lambda: CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
# Unresolveable substition
self.assertFalse(CommandAction.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''}))
self.assertFalse(CommandAction.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''}))
self.assertRaises(ValueError,
lambda: CommandAction.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''}))
self.assertRaises(ValueError,
lambda: CommandAction.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''}))
# missing tags are ok
self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'})
self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> 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(

View File

@ -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)