diff --git a/README.md b/README.md index 601d72ca80..59955e4146 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Optional: * [python-systemd package](https://www.freedesktop.org/software/systemd/python-systemd/index.html) - [dnspython](http://www.dnspython.org/) - [pyasyncore](https://pypi.org/project/pyasyncore/) and [pyasynchat](https://pypi.org/project/pyasynchat/) (normally bundled-in within fail2ban, for python 3.12+ only) +- [geoiplookup](https://linux.die.net/man/1/geoiplookup) +- [mmdblookup](https://maxmind.github.io/libmaxminddb/mmdblookup.html) To install: diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 97cd38b21c..935c627451 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -147,6 +147,14 @@ def beautify(self, response): for ip in response[:-1]: msg += "|- " + ip + "\n" msg += "`- " + response[-1] + elif inC[2] in ("ignoregeo", "addignoregeo", "delignoregeo"): + if len(response) == 0: + msg = "No GEO location is ignored" + else: + msg = "These GEO locations are ignored:\n" + for geo in response[:-1]: + msg += "|- " + geo + "\n" + msg += "`- " + response[-1] elif inC[2] in ("failregex", "addfailregex", "delfailregex", "ignoreregex", "addignoreregex", "delignoreregex"): if len(response) == 0: diff --git a/fail2ban/client/jailreader.py b/fail2ban/client/jailreader.py index e7242bfd4d..a21c5c0bb5 100644 --- a/fail2ban/client/jailreader.py +++ b/fail2ban/client/jailreader.py @@ -111,6 +111,7 @@ def _glob(path): "ignorecommand": ["string", None], "ignoreself": ["bool", None], "ignoreip": ["string", None], + "ignoregeo": ["string", None], "ignorecache": ["string", None], "filter": ["string", ""], "logtimezone": ["string", None], @@ -279,6 +280,8 @@ def convert(self, allow_no_files=False): logSys.warning(msg) elif opt == "ignoreip": stream.append(["set", self.__name, "addignoreip"] + splitwords(value)) + elif opt == "ignoregeo": + stream.append(["set", self.__name, "addignoregeo"] + splitwords(value)) elif opt not in JailReader._ignoreOpts: stream.append(["set", self.__name, opt, value]) # consider options order (after other options): diff --git a/fail2ban/protocol.py b/fail2ban/protocol.py index a81c665724..d0cb1ed8a4 100644 --- a/fail2ban/protocol.py +++ b/fail2ban/protocol.py @@ -88,6 +88,8 @@ def __getattr__(self, name): ["set ignoreself true|false", "allows the ignoring of own IP addresses"], ["set addignoreip ", "adds to the ignore list of "], ["set delignoreip ", "removes from the ignore list of "], +["set addignoregeo ", "adds to the ignore list of "], +["set delignoregeo ", "removes from the ignore list of "], ["set ignorecommand ", "sets ignorecommand of "], ["set ignorecache ", "sets ignorecache of "], ["set addlogpath ['tail']", "adds to the monitoring list of , optionally starting at the 'tail' of the file (default 'head')."], @@ -129,6 +131,7 @@ def __getattr__(self, name): ["get journalmatch", "gets the journal filter match for "], ["get ignoreself", "gets the current value of the ignoring the own IP addresses"], ["get ignoreip", "gets the list of ignored IP addresses for "], +["get ignoregeo", "gets the list of ignored GEO locations for "], ["get ignorecommand", "gets ignorecommand of "], ["get failregex", "gets the list of regular expressions which matches the failures for "], ["get ignoreregex", "gets the list of regular expressions which matches patterns to ignore for "], diff --git a/fail2ban/server/filter.py b/fail2ban/server/filter.py index c2b4886bcf..4849e76aff 100644 --- a/fail2ban/server/filter.py +++ b/fail2ban/server/filter.py @@ -29,6 +29,9 @@ import re import sys import time +import subprocess +import shutil + from .actions import Actions from .failmanager import FailManagerEmpty, FailManager @@ -83,6 +86,8 @@ def __init__(self, jail, useDns='warn'): ## The ignore IP list. self.__ignoreIpSet = set() self.__ignoreIpList = [] + ## The ignore GEO ip list. + self.__ignoreGeoSet = set() ## External command self.__ignoreCommand = False ## Cache for ignoreip: @@ -541,6 +546,33 @@ def logIgnoreIp(self, ip, log_ignore, ignore_source="unknown source"): def getIgnoreIP(self): return self.__ignoreIpList + list(self.__ignoreIpSet) + def addIgnoreGEO(self, geo): + # An empty string is always false + if geo == "": + return + + # Avoid exact duplicates + if geo in self.__ignoreGeoSet: + logSys.log(logging.MSG, " Ignore duplicate %r, already in geo ignore list", geo) + return + + # log and append to ignore list + logSys.debug(" Add %r to geo ignore list", geo) + self.__ignoreGeoSet.add(geo) + + def delIgnoreGEO(self, geo=None): + # clear all: + if geo is None: + self.__ignoreGeoSet.clear() + return + # delete by ip: + logSys.debug(" Remove %r from geo ignore list", geo) + if geo in self.__ignoreGeoSet: + self.__ignoreGeoSet.remove(geo) + + def getIgnoreGEO(self): + return list(self.__ignoreGeoSet) + ## # Check if IP address/DNS is in the ignore list. # @@ -579,6 +611,25 @@ def _inIgnoreIPList(self, ip, ticket, log_ignore=True): if self.__ignoreCache: c.set(key, True) return True + # check if the IP's geolocation is not on geo ignore list + + if self.__ignoreGeoSet: + geoipcc = None + geoip2cf = "/usr/share/GeoIP/GeoIP2-Country.mmdb" + + if shutil.which("mmdblookup") and os.path.isfile(geoip2cf): + geoipcc = str(subprocess.check_output(["mmdblookup","--file","/usr/share/GeoIP/GeoIP2-Country.mmdb","--ip",str(ip),"country","iso_code"])).split(" ")[2].replace("\"", "") + elif shutil.which("geoiplookup"): + if shutil.which("mmdblookup") and not os.path.isfile(geoip2cf): + logSys.warning("Found mmdblookup but cannot find mmdb country file at /usr/share/GeoIP/GeoIP2-Country.mmdb, using geoiplookup instead.") + geoipcc = str(subprocess.check_output(["geoiplookup",str(ip)])).split(" ")[3].replace(",","") + else: + logSys.warning("Cannot find geoiplookup or correctly set mmdblookup. Geolocation is unavailable. If you have mmdblookup installed, please check if /usr/share/GeoIP/GeoIP2-Country.mmdb file is available.") + + if geoipcc in self.__ignoreGeoSet: + self.logIgnoreIp(ip, log_ignore, ignore_source="geo-" + geoipcc) + return True + # check if the IP is covered by ignore IP (in set or in subnet/dns): if ip in self.__ignoreIpSet: self.logIgnoreIp(ip, log_ignore, ignore_source="ip") diff --git a/fail2ban/server/server.py b/fail2ban/server/server.py index 7c6fc2f78c..f544d4190e 100644 --- a/fail2ban/server/server.py +++ b/fail2ban/server/server.py @@ -370,6 +370,15 @@ def delIgnoreIP(self, name, ip): def getIgnoreIP(self, name): return self.__jails[name].filter.getIgnoreIP() + + def addIgnoreGEO(self, name, geo): + self.__jails[name].filter.addIgnoreGEO(geo) + + def delIgnoreGEO(self, name, geo): + self.__jails[name].filter.delIgnoreGEO(geo) + + def getIgnoreGEO(self, name): + return self.__jails[name].filter.getIgnoreGEO() def addLogPath(self, name, fileName, tail=False): filter_ = self.__jails[name].filter diff --git a/fail2ban/server/transmitter.py b/fail2ban/server/transmitter.py index 2893134499..660ed03aed 100644 --- a/fail2ban/server/transmitter.py +++ b/fail2ban/server/transmitter.py @@ -235,6 +235,16 @@ def __commandSet(self, command, multiple=False): self.__server.delIgnoreIP(name, value) if self.__quiet: return return self.__server.getIgnoreIP(name) + elif command[1] == "addignoregeo": + for value in command[2:]: + self.__server.addIgnoreGEO(name, value) + if self.__quiet: return + return self.__server.getIgnoreGEO(name) + elif command[1] == "delignoregeo": + value = command[2] + self.__server.delIgnoreGEO(name, value) + if self.__quiet: return + return self.__server.getIgnoreGEO(name) elif command[1] == "ignorecommand": value = command[2] self.__server.setIgnoreCommand(name, value) @@ -453,6 +463,8 @@ def __commandGet(self, command): return self.__server.getIgnoreSelf(name) elif command[1] == "ignoreip": return self.__server.getIgnoreIP(name) + elif command[1] == "ignoregeo": + return self.__server.getIgnoreGEO(name) elif command[1] == "ignorecommand": return self.__server.getIgnoreCommand(name) elif command[1] == "ignorecache": diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index ed68e7a5ba..c23db62728 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -229,6 +229,14 @@ adds to the ignore list of removes from the ignore list of .TP +\fBset addignoregeo \fR +adds to the ignore list of + +.TP +\fBset delignoreigeo \fR +removes from the ignore list +of +.TP \fBset ignorecommand \fR sets ignorecommand of .TP @@ -392,6 +400,10 @@ ignoring the own IP addresses gets the list of ignored IP addresses for .TP +\fBget ignoregeo\fR +gets the list of ignored GEO +addresses for +.TP \fBget ignorecommand\fR gets ignorecommand of .TP diff --git a/man/jail.conf.5 b/man/jail.conf.5 index 363e996b37..1b63612a07 100644 --- a/man/jail.conf.5 +++ b/man/jail.conf.5 @@ -246,6 +246,9 @@ boolean value (default true) indicates the banning of own IP addresses should be .B ignoreip list of IPs not to ban. They can include a DNS resp. CIDR mask too. The option affects additionally to \fBignoreself\fR (if true) and don't need to contain own DNS resp. IPs of the running host. .TP +.B ignoregeo +list of GEO location codes of IPs not to ban. For this to work, you should have a `geoiplookup` or `mmdblookup` or other program with the same name installed. This feature was only tested with Maxmind's `geoiplookup` or `mmdblookup`. +.TP .B ignorecommand command that is executed to determine if the current candidate IP for banning (or failure-ID for raw IDs) should not be banned. This option operates alongside the \fBignoreself\fR and \fBignoreip\fR options. It is executed first, only if neither \fBignoreself\fR nor \fBignoreip\fR match the criteria. .br