diff --git a/fail2ban/server/failregex.py b/fail2ban/server/failregex.py index 75e64c46..6076acc3 100644 --- a/fail2ban/server/failregex.py +++ b/fail2ban/server/failregex.py @@ -43,8 +43,8 @@ class Regex: def __init__(self, regex): self._matchCache = None # Perform shortcuts expansions. - # Replace "" with default regular expression for host. - regex = regex.replace("", "(?:::f{4,6}:)?(?P[\w\-.^_]*\w)") + # Resolve "" tag using default regular expression for host: + regex = Regex._resolveHostTag(regex) # Replace "" with regular expression for multiple lines. regexSplit = regex.split("") regex = regexSplit[0] @@ -61,6 +61,20 @@ class Regex: def __str__(self): return "%s(%r)" % (self.__class__.__name__, self._regex) + + @staticmethod + def _resolveHostTag(regex): + # Replace "" with default regular expression for host: + # Other candidates (see gh-1374 for the discussion about): + # differentiate: r"""(?:(?:::f{4,6}:)?(?P(?:\d{1,3}\.){3}\d{1,3})|\[?(?P(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}|(?<=:):))\]?|(?P[\w\-.^_]*\w))""" + # expected many changes in filter, failregex, etc... + # simple: r"""(?:::f{4,6}:)?(?P[\w\-.^_:]*\w)""" + # not good enough, if not precise expressions around , because for example will match '1.2.3.4:23930' as ip-address; + # Todo: move this functionality to filter reader, as default replacement, + # make it configurable (via jail/filter configs) + return regex.replace("", + r"""(?:::f{4,6}:)?(?P(?:\d{1,3}\.){3}\d{1,3}|\[?(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}\]?|(?<=:):)|[\w\-.^_]*\w)""") + ## # Gets the regular expression. # diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index 8021b1d8..bca03428 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -103,7 +103,7 @@ class DNSUtils: # Search for plain IP plainIP = IPAddr.searchIP(text) if plainIP is not None: - ip = IPAddr(plainIP.group(0)) + ip = IPAddr(plainIP) if ip.isValid: ipList.append(ip) @@ -127,9 +127,8 @@ class DNSUtils: class IPAddr(object): """Encapsulate functionality for IPv4 and IPv6 addresses """ - - IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") - IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") + IP_4_6_CRE = re.compile( + r"""^(?:(?P(?:\d{1,3}\.){3}\d{1,3})|\[?(?P(?:[0-9a-fA-F]{1,4}::?|::){1,7}(?:[0-9a-fA-F]{1,4}|(?<=:):))\]?)$""") # An IPv4 compatible IPv6 to be reused (see below) IP6_4COMPAT = None @@ -139,19 +138,33 @@ class IPAddr(object): # todo: make configurable the expired time and max count of cache entries: CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60) - def __new__(cls, ipstring, cidr=-1): - # already correct IPAddr - args = (ipstring, cidr) + def __new__(cls, ipstr, cidr=-1): + # check already cached as IPAddr + args = (ipstr, cidr) ip = IPAddr.CACHE_OBJ.get(args) if ip is not None: return ip + # wrap mask to cidr (correct plen): + if cidr == -1: + ipstr, cidr = IPAddr.__wrap_ipstr(ipstr) + args = (ipstr, cidr) + # check cache again: + if cidr != -1: + ip = IPAddr.CACHE_OBJ.get(args) + if ip is not None: + return ip ip = super(IPAddr, cls).__new__(cls) - ip.__init(ipstring, cidr) + ip.__init(ipstr, cidr) IPAddr.CACHE_OBJ.set(args, ip) return ip @staticmethod def __wrap_ipstr(ipstr): + # because of standard spelling of IPv6 (with port) enclosed in brackets ([ipv6]:port), + # remove they now (be sure the inside failregex uses this for IPv6 (has \[?...\]?) + if len(ipstr) > 2 and ipstr[0] == '[' and ipstr[-1] == ']': + ipstr = ipstr[1:-1] + # test mask: if "/" not in ipstr: return ipstr, -1 s = ipstr.split('/', 1) @@ -173,9 +186,6 @@ class IPAddr(object): self._maskplen = None self._raw = "" - if cidr == -1: - ipstr, cidr = self.__wrap_ipstr(ipstr) - for family in [socket.AF_INET, socket.AF_INET6]: try: binary = socket.inet_pton(family, ipstr) @@ -376,17 +386,17 @@ class IPAddr(object): @property def maskplen(self): - plen = 0 + mplen = 0 if self._maskplen is not None: - return self._plen - maddr = self.addr + return self._maskplen + maddr = self._addr while maddr: if not (maddr & 0x80000000): raise ValueError("invalid mask %r, no plen representation" % (str(self),)) maddr = (maddr << 1) & 0xFFFFFFFFL - plen += 1 - self._maskplen = plen - return plen + mplen += 1 + self._maskplen = mplen + return mplen @staticmethod def masktoplen(mask): @@ -400,10 +410,13 @@ class IPAddr(object): def searchIP(text): """Search if text is an IP address, and return it if so, else None """ - match = IPAddr.IP_CRE.match(text) + match = IPAddr.IP_4_6_CRE.match(text) if not match: - match = IPAddr.IP6_CRE.match(text) - return match if match else None + return None + ipstr = match.group('IPv4') + if ipstr != '': + return ipstr + return match.group('IPv6') # An IPv4 compatible IPv6 to be reused diff --git a/fail2ban/tests/files/logs/apache-auth b/fail2ban/tests/files/logs/apache-auth index 5b7b3c48..29de57eb 100644 --- a/fail2ban/tests/files/logs/apache-auth +++ b/fail2ban/tests/files/logs/apache-auth @@ -8,6 +8,8 @@ # failJSON: { "time": "2013-07-11T01:21:43", "match": true , "host": "194.228.20.113" } [Thu Jul 11 01:21:43 2013] [error] [client 194.228.20.113] user dsfasdf not found: / +# failJSON: { "time": "2013-07-11T01:21:44", "match": true , "host": "2606:2800:220:1:248:1893:25c8:1946" } +[Thu Jul 11 01:21:44 2013] [error] [client 2606:2800:220:1:248:1893:25c8:1946] user test-ipv6 not found: / # The failures below use the configuration described in fail2ban/tests/files/config/apache-auth # @@ -56,6 +58,8 @@ # failJSON: { "time": "2013-07-20T22:11:43", "match": true , "host": "127.0.0.1" } [Sat Jul 20 22:11:43.147674 2013] [authz_owner:error] [pid 17540:tid 140122922129152] [client 127.0.0.1:51548] AH01637: Authorization of user username to access /basic/authz_owner/cant_get_me.html failed, reason: file owner dan does not match +# failJSON: { "time": "2013-07-20T22:11:44", "match": true , "host": "2606:2800:220:1:248:1893:25c8:1946" } +[Sat Jul 20 22:11:44.147674 2013] [authz_owner:error] [pid 17540:tid 140122922129152] [client [2606:2800:220:1:248:1893:25c8:1946]:51548] AH01637: Authorization of user test-ipv6 to access /basic/authz_owner/cant_get_me.html failed, reason: file owner dan does not match # wget --http-user=username --http-password=password http://localhost/basic/authz_owner/cant_get_me.html -O /dev/null # failJSON: { "time": "2013-07-20T21:42:44", "match": true , "host": "127.0.0.1" } diff --git a/fail2ban/tests/files/logs/sshd b/fail2ban/tests/files/logs/sshd index 7baf4be7..89e4a3e2 100644 --- a/fail2ban/tests/files/logs/sshd +++ b/fail2ban/tests/files/logs/sshd @@ -3,6 +3,8 @@ Jun 21 16:47:48 digital-mlhhyiqscv sshd[13709]: error: PAM: Authentication failure for myhlj1374 from 192.030.0.6 # failJSON: { "time": "2005-05-29T20:56:52", "match": true , "host": "example.com" } May 29 20:56:52 imago sshd[28732]: error: PAM: Authentication failure for stefanor from example.com +# failJSON: { "time": "2005-05-29T20:56:56", "match": true , "host": "2606:2800:220:1:248:1893:25c8:1946" } +May 29 20:56:56 imago sshd[28732]: error: PAM: Authentication failure for test-ipv6 from 2606:2800:220:1:248:1893:25c8:1946 #2 # failJSON: { "time": "2005-02-25T14:34:10", "match": true , "host": "194.117.26.69" } diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index 893976be..1c05f5e8 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -458,9 +458,9 @@ class Transmitter(TransmitterBase): "failed attempt from again", ], [ - "user john at (?:::f{4,6}:)?(?P[\w\-.^_]*\\w)", - "Admin user login from (?:::f{4,6}:)?(?P[\w\-.^_]*\\w)", - "failed attempt from (?:::f{4,6}:)?(?P[\w\-.^_]*\\w) again", + "user john at %s" % (Regex._resolveHostTag('')), + "Admin user login from %s" % (Regex._resolveHostTag('')), + "failed attempt from %s again" % (Regex._resolveHostTag('')), ], self.jailName ) @@ -483,7 +483,7 @@ class Transmitter(TransmitterBase): ], [ "user john", - "Admin user login from (?:::f{4,6}:)?(?P[\w\-.^_]*\\w)", + "Admin user login from %s" % (Regex._resolveHostTag('')), "Dont match me!", ], self.jailName diff --git a/fail2ban/tests/utils.py b/fail2ban/tests/utils.py index e97daebf..a0036979 100644 --- a/fail2ban/tests/utils.py +++ b/fail2ban/tests/utils.py @@ -101,6 +101,16 @@ def initTests(opts): c.set('192.0.2.%s' % i, None) c.set('198.51.100.%s' % i, None) c.set('203.0.113.%s' % i, None) + if unittest.F2B.no_network: # pragma: no cover + # precache all wrong dns to ip's used in test cases: + c = DNSUtils.CACHE_nameToIp + for i in ( + ('999.999.999.999', []), + ('abcdef.abcdef', []), + ('192.168.0.', []), + ('failed.dns.ch', []), + ): + c.set(*i) def mtimesleep():