diff --git a/.codespellrc b/.codespellrc index a5dd775e..9ff01cdc 100644 --- a/.codespellrc +++ b/.codespellrc @@ -9,4 +9,4 @@ check-hidden = true ignore-regex = (\b([A-Z][A-Z][A-Z]+|gir\.st)\b)|\[[a-zA-Z]+\][a-z]+\b|[a-z]+://\S+|.*codespell-ignore.* # some oddly named variables, some names, etc # wee -- comes in regex etc for weeks -ignore-words-list = theis,timere,alls,wee,wight,ans,re-use +ignore-words-list = assertIn,theis,timere,alls,wee,wight,ans,re-use diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 543f316a..74c8eb14 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,5 @@ # These are supported funding model platforms github: [sebres] -custom: [paypal.me/sebres] +custom: [https://paypal.me/sebres] +liberapay: sebres diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30e38f7d..78b3eb08 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13.0-alpha.2', pypy3.10] + python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12', '3.13.0-beta.3', pypy3.10] fail-fast: false # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -79,7 +79,7 @@ jobs: cd "$GITHUB_WORKSPACE" _debug() { echo -n "$1 "; err=$("${@:2}" 2>&1) && echo 'OK' || echo -e "FAIL\n$err"; } # (debug) output current preferred encoding: - _debug 'Encodings:' python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))' + echo 'Encodings:' $(python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))') # (debug) backend availabilities: echo 'Backends:' _debug '- systemd:' python -c 'from fail2ban.server.filtersystemd import FilterSystemd' diff --git a/ChangeLog b/ChangeLog index 2c95dcac..da45a97d 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,33 @@ Fail2Ban: Changelog =================== +ver. 1.1.1-dev-1 (20??/??/??) - development nightly edition +----------- + +### Fixes +* `jail.conf`: + - default banactions need to be specified in `paths-*.conf` (maintainer level) now + - since stock fail2ban includes `paths-debian.conf` by default, banactions are `nftables` + (can be overwritten in `jail.local` by user) +* `paths-debian.conf`: + - default banactions are `nftables` + - sshd backend switched to `systemd` (gh-3292) +* `action.d/firewallcmd-ipset.conf`: + - rename `ipsettype` to `ipsetbackend` (gh-2620), parameter `ipsettype` will be used now to the real set type (gh-3760) +* `filter.d/apache-overflows.conf` - consider AH10244: invalid URI path (gh-3778) +* `filter.d/postfix.conf` - consider CONNECT and other rejected commands as a valid `_pref` (gh-3800) +* `filter.d/recidive.conf` - restore possibility to set jail name in the filter, _jailname is positive now (gh-3769) +* `filter.d/roundcube-auth.conf` - improved RE better matching log format of roundcube version 1.4+ (gh-3816) +* `filter.d/sshd.conf` - adapted to conform possible new daemon name sshd-session, since OpenSSH 9.8 + several log messages will be tagged with as originating from a process named "sshd-session" rather than "sshd" (gh-3782) + +### New Features and Enhancements +* `action.d/*-ipset.conf`: + - parameter `ipsettype` to set type of ipset, e. g. hash:ip, hash:net, etc (gh-3760) +* `action.d/firewallcmd-rich-*.conf` - fixed incorrect quoting, disabling port variable expansion + by substitution of rich rule (gh-3815) +* `filter.d/proxmox.conf` - add support to Proxmox Web GUI (gh-2966) + ver. 1.1.0 (2024/04/25) - object-found--norad-59479-cospar-2024-069a--altitude-36267km ----------- diff --git a/README.md b/README.md index 601d72ca..a5aa646c 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,13 @@ and the website: https://www.fail2ban.org Installation: ------------- -Fail2Ban is likely already packaged for your Linux distribution and [can installed with a simple command](https://github.com/fail2ban/fail2ban/wiki/How-to-install-fail2ban-packages). +Fail2Ban is likely already packaged for your Linux distribution and [can be installed with a simple command](https://github.com/fail2ban/fail2ban/wiki/How-to-install-fail2ban-packages). If your distribution is not listed, you can install from GitHub: Required: - [Python >= 3.5](https://www.python.org) or [PyPy3](https://pypy.org) -- python-setuptools, python-distutils (or python3-setuptools) for installation from source +- python-setuptools (or python3-setuptools) for installation from source Optional: - [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require: @@ -52,7 +52,7 @@ To install: cd fail2ban-master sudo python setup.py install -Alternatively, you can clone the source from GitHub to a directory of Your choice, and do the install from there. Pick the correct branch, for example, master or 0.11 +Alternatively, you can clone the source from GitHub to a directory of your choice, and do the install from there. Pick the correct branch, for example, master or 0.11 git clone https://github.com/fail2ban/fail2ban.git cd fail2ban diff --git a/config/action.d/abuseipdb.conf b/config/action.d/abuseipdb.conf index ed958c86..d0d4a99b 100644 --- a/config/action.d/abuseipdb.conf +++ b/config/action.d/abuseipdb.conf @@ -80,7 +80,7 @@ actioncheck = # use my (Shaun's) helper PHP script by commenting out the first #actionban # line below, uncommenting the second one, and pointing the URL at # wherever you install the helper script. For the PHP helper script, see -# +# # # Tags: See jail.conf(5) man page # Values: CMD diff --git a/config/action.d/blocklist_de.conf b/config/action.d/blocklist_de.conf index ba6d427b..41c35497 100644 --- a/config/action.d/blocklist_de.conf +++ b/config/action.d/blocklist_de.conf @@ -30,6 +30,9 @@ [Definition] +# bypass reporting of restored (already reported) tickets: +norestored = 1 + # Option: actionstart # Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). # Values: CMD diff --git a/config/action.d/firewallcmd-ipset.conf b/config/action.d/firewallcmd-ipset.conf index c5282c62..ed498084 100644 --- a/config/action.d/firewallcmd-ipset.conf +++ b/config/action.d/firewallcmd-ipset.conf @@ -18,24 +18,24 @@ before = firewallcmd-common.conf [Definition] -actionstart = /actionstart> +actionstart = /actionstart> firewall-cmd --direct --add-rule filter 0 -m set --match-set src -j -actionflush = /actionflush> +actionflush = /actionflush> actionstop = firewall-cmd --direct --remove-rule filter 0 -m set --match-set src -j - /actionstop> + /actionstop> -actionban = /actionban> +actionban = /actionban> # actionprolong = %(actionban)s -actionunban = /actionunban> +actionunban = /actionunban> -[ipstype_ipset] +[ipsbackend_ipset] -actionstart = ipset -exist create hash:ip timeout maxelem +actionstart = ipset -exist create timeout maxelem actionflush = ipset flush @@ -45,9 +45,9 @@ actionban = ipset -exist add timeout actionunban = ipset -exist del -[ipstype_firewalld] +[ipsbackend_firewalld] -actionstart = firewall-cmd --direct --new-ipset= --type=hash:ip --option=timeout= --option=maxelem= +actionstart = firewall-cmd --direct --new-ipset= --type= --option=timeout= --option=maxelem= # TODO: there doesn't seem to be an explicit way to invoke the ipset flush function using firewall-cmd actionflush = @@ -60,6 +60,11 @@ actionunban = firewall-cmd --ipset= --remove-entry= [Init] +# Option: ipsettype +# Notes: specifies type of set, see `man --pager='less -p "^SET TYPES"' ipset` for details +# Values: hash:ip, hash:net, etc... Default: hash:ip +ipsettype = hash:ip + # Option: chain # Notes specifies the iptables chain to which the fail2ban rules should be # added @@ -87,11 +92,11 @@ maxelem = 65536 # banaction = %(known/banaction)s[ipsettime=''] timeout-bantime = $([ "" -le 2147483 ] && echo "" || echo 0) -# Option: ipsettype -# Notes.: defines type of ipset used for match-set (firewalld or ipset) +# Option: ipsetbackend +# Notes.: defines the backend of ipset used for match-set (firewalld or ipset) # Values: firewalld or ipset # Default: ipset -ipsettype = ipset +ipsetbackend = ipset # Option: actiontype # Notes.: defines additions to the blocking rule diff --git a/config/action.d/firewallcmd-rich-rules.conf b/config/action.d/firewallcmd-rich-rules.conf index 75a27d88..352034b4 100644 --- a/config/action.d/firewallcmd-rich-rules.conf +++ b/config/action.d/firewallcmd-rich-rules.conf @@ -35,7 +35,7 @@ actioncheck = # # Because rich rules can only handle single or a range of ports we must split ports and execute the command for each port. Ports can be single and ranges separated by a comma or space for an example: http, https, 22-60, 18 smtp -fwcmd_rich_rule = rule family='' source address='' port port='$p' protocol='' %(rich-suffix)s +fwcmd_rich_rule = rule family=\"\" source address=\"\" port port=\"$p\" protocol=\"\" %(rich-suffix)s actionban = ports=""; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done diff --git a/config/action.d/iptables-ipset.conf b/config/action.d/iptables-ipset.conf index 07f89415..89d90142 100644 --- a/config/action.d/iptables-ipset.conf +++ b/config/action.d/iptables-ipset.conf @@ -24,7 +24,7 @@ before = iptables.conf # Notes.: command executed on demand at the first ban (or at the start of Fail2Ban if actionstart_on_demand is set to false). # Values: CMD # -actionstart = ipset -exist create hash:ip timeout maxelem +actionstart = ipset -exist create timeout maxelem <_ipt_add_rules> # Option: actionflush @@ -66,6 +66,11 @@ rule-jump = -m set --match-set src -j [Init] +# Option: ipsettype +# Notes: specifies type of set, see `man --pager='less -p "^SET TYPES"' ipset` for details +# Values: hash:ip, hash:net, etc... Default: hash:ip +ipsettype = hash:ip + # Option: default-ipsettime # Notes: specifies default timeout in seconds (handled default ipset timeout only) # Values: [ NUM ] Default: 0 (no timeout, managed by fail2ban by unban) diff --git a/config/action.d/shorewall-ipset-proto6.conf b/config/action.d/shorewall-ipset-proto6.conf index fade8107..72e3fea9 100644 --- a/config/action.d/shorewall-ipset-proto6.conf +++ b/config/action.d/shorewall-ipset-proto6.conf @@ -51,7 +51,7 @@ # Values: CMD # actionstart = if ! ipset -quiet -name list f2b- >/dev/null; - then ipset -quiet -exist create f2b- hash:ip timeout maxelem ; + then ipset -quiet -exist create f2b- timeout maxelem ; fi # Option: actionstop @@ -94,6 +94,11 @@ timeout-bantime = $([ "" -le 2147483 ] && echo "" || echo 0) [Init] +# Option: ipsettype +# Notes: specifies type of set, see `man --pager='less -p "^SET TYPES"' ipset` for details +# Values: hash:ip, hash:net, etc... Default: hash:ip +ipsettype = hash:ip + # Option: maxelem # Notes: maximal number of elements which can be stored in the ipset # You may want to increase this for long-duration/high-volume jails diff --git a/config/filter.d/apache-overflows.conf b/config/filter.d/apache-overflows.conf index 0f54da11..b9bfc364 100644 --- a/config/filter.d/apache-overflows.conf +++ b/config/filter.d/apache-overflows.conf @@ -8,7 +8,7 @@ before = apache-common.conf [Definition] -failregex = ^%(_apache_error_client)s (?:(?:AH001[23][456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b) +failregex = ^%(_apache_error_client)s (?:(?:AH(?:001[23][456]|10244): )?[Ii]nvalid (method|URI)\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b) ignoreregex = diff --git a/config/filter.d/postfix.conf b/config/filter.d/postfix.conf index 5497504e..a1882473 100644 --- a/config/filter.d/postfix.conf +++ b/config/filter.d/postfix.conf @@ -12,7 +12,7 @@ before = common.conf _daemon = postfix(-\w+)?/[^/\[:\s]+(?:/smtp[ds])? _port = (?::\d+)? -_pref = [A-Z]{4} +_pref = [A-Z]{4,} prefregex = ^%(__prefix_line)s> .+$ diff --git a/config/filter.d/proxmox.conf b/config/filter.d/proxmox.conf new file mode 100644 index 00000000..8d7975b2 --- /dev/null +++ b/config/filter.d/proxmox.conf @@ -0,0 +1,20 @@ +# Fail2Ban filter for Proxmox Web GUI +# +# Jail example: +# [proxmox] +# enabled = true +# port = https,http,8006 +# filter = proxmox +# logpath = /var/log/daemon.log +# maxretry = 3 +# # 1 hour +# bantime = 3600 + +[Definition] + +_daemon = pvedaemon + +failregex = ^\s*\S+ %(_daemon)s\[\d+\]: authentication failure; rhost= user=\S+ + +ignoreregex = + diff --git a/config/filter.d/recidive.conf b/config/filter.d/recidive.conf index 86d939bb..eba9a048 100644 --- a/config/filter.d/recidive.conf +++ b/config/filter.d/recidive.conf @@ -24,14 +24,15 @@ before = common.conf _daemon = (?:fail2ban(?:-server|\.actions)\s*) # The name of the jail that this filter is used for. In jail.conf, name the jail using -# this filter 'recidive', or supply another name with `filter = recidive[_jailname="jail"]` -_jailname = recidive +# this filter 'recidive', or supply another name with `filter = recidive[_jailname="jail"]`, +# default all jails excepting recidive +_jailname = (?!recidive\])[^\]]* -failregex = ^%(__prefix_line)s(?:\s*fail2ban\.actions\s*%(__pid_re)s?:\s+)?NOTICE\s+\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+\s*$ +failregex = ^%(__prefix_line)s(?:\s*fail2ban\.actions\s*%(__pid_re)s?:\s+)?NOTICE\s+\[<_jailname>\]\s+Ban\s+ [lt_short] _daemon = (?:fail2ban(?:-server|\.actions)?\s*) -failregex = ^%(__prefix_line)s(?:\s*fail2ban(?:\.actions)?\s*%(__pid_re)s?:\s+)?(?:NOTICE\s+)?\[(?!%(_jailname)s\])(?:.*)\]\s+Ban\s+\s*$ +failregex = ^%(__prefix_line)s(?:\s*fail2ban(?:\.actions)?\s*%(__pid_re)s?:\s+)?(?:NOTICE\s+)?\[<_jailname>\]\s+Ban\s+ [lt_journal] _daemon = diff --git a/config/filter.d/roundcube-auth.conf b/config/filter.d/roundcube-auth.conf index 7eed8adb..e2ac267e 100644 --- a/config/filter.d/roundcube-auth.conf +++ b/config/filter.d/roundcube-auth.conf @@ -13,10 +13,9 @@ before = common.conf [Definition] -prefregex = ^\s*(\[\])?(%(__hostname)s\s*(?:roundcube(?:\[(\d*)\])?:)?\s*(<[\w]+>)? IMAP Error)?: .+$ +prefregex = ^\s*(\[\])?(%(__hostname)s\s*(?:roundcube(?:\[(\d*)\])?:)?\s*(<[\w]+>)? IMAP Error)?: (?:<[\w]+> )?.+$ -failregex = ^(?:FAILED login|Login failed) for .* from (?:(?:\([^\)]*\))?\. (?:(?! from ).)*(?: user=(?P=user))? in \S+\.php on line \d+ \(\S+ \S+\))?$ - ^(?:<[\w]+> )?Failed login for .* from in session \w+( \(error: \d\))?$ +failregex = ^(?:Login failed|(?i:Failed) login) for (?:(?P\S+)|.*) (?:against \S+ )?from (?:(?:\([^\)]*\))?\.(?! from ) (?(simple)(?:\S+(?! from ) )*|(?:(?! from ).)*(?: user=(?P=user))? )in \S+\.php on line \d+| in session \w+)?(?: \([^\)]*\))?$ ignoreregex = Could not connect to .* Connection refused diff --git a/config/filter.d/sshd.conf b/config/filter.d/sshd.conf index a954774c..206b913a 100644 --- a/config/filter.d/sshd.conf +++ b/config/filter.d/sshd.conf @@ -16,7 +16,7 @@ before = common.conf [DEFAULT] -_daemon = sshd +_daemon = sshd(?:-session)? # optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: " __pref = (?:(?:error|fatal): (?:PAM: )?)? diff --git a/config/jail.conf b/config/jail.conf index a1ced24d..edf3e676 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -990,3 +990,6 @@ logpath = /var/log/monitorix-httpd port = 1080 logpath = %(syslog_daemon)s +[proxmox] +port = https,http,8006 +logpath = /var/log/daemon.log diff --git a/doc/conf.py b/doc/conf.py index 20845a5a..48d27f70 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -47,12 +47,9 @@ copyright = u'2014' # from fail2ban.version import version as fail2ban_version -from distutils.version import LooseVersion - -fail2ban_loose_version = LooseVersion(fail2ban_version) # The short X.Y version. -version = ".".join(str(_) for _ in fail2ban_loose_version.version[:2]) +version = ".".join(str(_) for _ in fail2ban_version.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = fail2ban_version diff --git a/fail2ban/client/beautifier.py b/fail2ban/client/beautifier.py index 7ef173a6..21c49b94 100644 --- a/fail2ban/client/beautifier.py +++ b/fail2ban/client/beautifier.py @@ -21,8 +21,10 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __license__ = "GPL" +import sys + from ..exceptions import UnknownJailException, DuplicateJailException -from ..helpers import getLogger, logging +from ..helpers import getLogger, logging, PREFER_ENC # Gets the instance of the logger. logSys = getLogger(__name__) @@ -36,6 +38,11 @@ logSys = getLogger(__name__) class Beautifier: + stdoutEnc = PREFER_ENC + if sys.stdout and sys.stdout.encoding is not None: + stdoutEnc = sys.stdout.encoding + encUtf = 1 if stdoutEnc.lower() == 'utf-8' else 0 + def __init__(self, cmd = None): self.__inputCmd = cmd @@ -104,7 +111,11 @@ class Beautifier: jail_stat(j, " " if i == len(jstat) else " | ") msg = "\n".join(msg) elif inC[0:1] == ['stats'] or inC[0:1] == ['statistics']: - def _statstable(response): + chrTable = [ + ['|', '-', '|', 'x', 'x', '-', '|', '-'], ## ascii + ["\u2551", "\u2550", "\u255F", "\u256B", "\u256C", "\u2569", "\u2502", "\u2500"] ## utf-8 + ]; + def _statstable(response, ct): tophead = ["Jail", "Backend", "Filter", "Actions"] headers = ["", "", "cur", "tot", "cur", "tot"] minlens = [8, 8, 3, 3, 3, 3] @@ -120,29 +131,31 @@ class Beautifier: f = "%%%ds" if ralign[i] else "%%-%ds" rfmt.append(f % lens[i]) hfmt.append(f % lens[i]) - rfmt = [rfmt[0], rfmt[1], "%s \u2502 %s" % (rfmt[2], rfmt[3]), "%s \u2502 %s" % (rfmt[4], rfmt[5])] - hfmt = [hfmt[0], hfmt[1], "%s \u2502 %s" % (hfmt[2], hfmt[3]), "%s \u2502 %s" % (hfmt[4], hfmt[5])] + rfmt = [rfmt[0], rfmt[1], "%s %s %s" % (rfmt[2], ct[6], rfmt[3]), "%s %s %s" % (rfmt[4], ct[6], rfmt[5])] + hfmt = [hfmt[0], hfmt[1], "%s %s %s" % (hfmt[2], ct[6], hfmt[3]), "%s %s %s" % (hfmt[4], ct[6], hfmt[5])] tlens = [lens[0], lens[1], 3 + lens[2] + lens[3], 3 + lens[4] + lens[5]] tfmt = [hfmt[0], hfmt[1], "%%-%ds" % (tlens[2],), "%%-%ds" % (tlens[3],)] tsep = tfmt[0:2] - rfmt = " \u2551 ".join(rfmt) - hfmt = " \u2551 ".join(hfmt) - tfmt = " \u2551 ".join(tfmt) - tsep = " \u2551 ".join(tsep) - separator = ((tsep % tuple(tophead[0:2])) + " \u255F\u2500" + - ("\u2500\u256B\u2500".join(['\u2500' * n for n in tlens[2:]])) + '\u2500') + rfmt = (" "+ct[0]+" ").join(rfmt) + hfmt = (" "+ct[0]+" ").join(hfmt) + tfmt = (" "+ct[0]+" ").join(tfmt) + tsep = (" "+ct[0]+" ").join(tsep) + separator = ((tsep % tuple(tophead[0:2])) + " "+ct[2]+ct[7] + + ((ct[7]+ct[3]+ct[7]).join([ct[7] * n for n in tlens[2:]])) + ct[7]) ret = [] - ret.append(tfmt % tuple(["", ""]+tophead[2:])) - ret.append(separator) - ret.append(hfmt % tuple(headers)) - separator = "\u2550\u256C\u2550".join(['\u2550' * n for n in tlens]) + '\u2550' - ret.append(separator) + ret.append(" "+tfmt % tuple(["", ""]+tophead[2:])) + ret.append(" "+separator) + ret.append(" "+hfmt % tuple(headers)) + separator = (ct[1]+ct[4]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1] + ret.append(ct[1]+separator) for row in rows: - ret.append(rfmt % tuple(row)) - separator = "\u2550\u2569\u2550".join(['\u2550' * n for n in tlens]) + '\u2550' - ret.append(separator) + ret.append(" "+rfmt % tuple(row)) + separator = (ct[1]+ct[5]+ct[1]).join([ct[1] * n for n in tlens]) + ct[1] + ret.append(ct[1]+separator) return ret - msg = "\n".join(_statstable(response)) + if not response: + return "No jails found." + msg = "\n".join(_statstable(response, chrTable[self.encUtf])) elif len(inC) < 2: pass # to few cmd args for below elif inC[1] == "syslogsocket": diff --git a/fail2ban/helpers.py b/fail2ban/helpers.py index fe62ae1e..0d7d8ba9 100644 --- a/fail2ban/helpers.py +++ b/fail2ban/helpers.py @@ -40,6 +40,14 @@ except: _libcap = None +# some modules (like pyinotify, see #3487) may have dependency to asyncore, so ensure we've a path +# to compat folder, otherwise python 3.12+ could miss them: +def __extend_compat_path(): + cp = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'compat') + if cp not in sys.path: + sys.path.append(cp) +__extend_compat_path() + PREFER_ENC = locale.getpreferredencoding() # correct preferred encoding if lang not set in environment: if PREFER_ENC.startswith('ANSI_'): # pragma: no cover diff --git a/fail2ban/server/asyncserver.py b/fail2ban/server/asyncserver.py index 0c36d846..62d6cce5 100644 --- a/fail2ban/server/asyncserver.py +++ b/fail2ban/server/asyncserver.py @@ -25,14 +25,6 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier" __license__ = "GPL" from pickle import dumps, loads, HIGHEST_PROTOCOL -try: - import asynchat -except ImportError: - from ..compat import asynchat -try: - import asyncore -except ImportError: - from ..compat import asyncore import errno import fcntl import os @@ -45,6 +37,13 @@ from .utils import Utils from ..protocol import CSPROTO from ..helpers import logging, getLogger, formatExceptionInfo +# load asyncore and asynchat after helper to ensure we've a path to compat folder: +import asynchat +if asynchat.asyncore: + asyncore = asynchat.asyncore +else: # pragma: no cover - normally unreachable + import asyncore + # Gets the instance of the logger. logSys = getLogger(__name__) diff --git a/fail2ban/server/filterpyinotify.py b/fail2ban/server/filterpyinotify.py index 81bc7de3..9fcbcb56 100644 --- a/fail2ban/server/filterpyinotify.py +++ b/fail2ban/server/filterpyinotify.py @@ -24,22 +24,18 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y __license__ = "GPL" import logging -from distutils.version import LooseVersion import os from os.path import dirname, sep as pathsep -import pyinotify - from .failmanager import FailManagerEmpty from .filter import FileFilter from .mytime import MyTime, time from .utils import Utils from ..helpers import getLogger - -if not hasattr(pyinotify, '__version__') \ - or LooseVersion(pyinotify.__version__) < '0.8.3': # pragma: no cover - raise ImportError("Fail2Ban requires pyinotify >= 0.8.3") +# pyinotify may have dependency to asyncore, so import it after helper to ensure +# we've a path to compat folder: +import pyinotify # Verify that pyinotify is functional on this system # Even though imports -- might be dysfunctional, e.g. as on kfreebsd diff --git a/fail2ban/server/filtersystemd.py b/fail2ban/server/filtersystemd.py index 5aea9fda..abd66e1f 100644 --- a/fail2ban/server/filtersystemd.py +++ b/fail2ban/server/filtersystemd.py @@ -24,11 +24,8 @@ __license__ = "GPL" import os import time -from distutils.version import LooseVersion from systemd import journal -if LooseVersion(getattr(journal, '__version__', "0")) < '204': - raise ImportError("Fail2Ban requires systemd >= 204") from .failmanager import FailManagerEmpty from .filter import JournalFilter, Filter diff --git a/fail2ban/tests/clientbeautifiertestcase.py b/fail2ban/tests/clientbeautifiertestcase.py index defedbe1..5fcb2404 100644 --- a/fail2ban/tests/clientbeautifiertestcase.py +++ b/fail2ban/tests/clientbeautifiertestcase.py @@ -34,6 +34,7 @@ class BeautifierTest(unittest.TestCase): """ Call before every test case """ super(BeautifierTest, self).setUp() self.b = Beautifier() + self.b.encUtf = 0; ## we prefer ascii in test suite (see #3750) def tearDown(self): """ Call after every test case """ @@ -170,22 +171,25 @@ class BeautifierTest(unittest.TestCase): def testStatusStats(self): self.b.setInputCmd(["stats"]) + ## no jails: + self.assertEqual(self.b.beautify({}), "No jails found.") + ## 3 jails: response = { "ssh": ["systemd", (3, 6), (12, 24)], "exim4": ["pyinotify", (6, 12), (20, 20)], "jail-with-long-name": ["polling", (0, 0), (0, 0)] } output = ("" - + " ? ? Filter ? Actions \n" - + "Jail ? Backend ????????????????????????\n" - + " ? ? cur ? tot ? cur ? tot\n" - + "????????????????????????????????????????????????????????\n" - + "ssh ? systemd ? 3 ? 6 ? 12 ? 24\n" - + "exim4 ? pyinotify ? 6 ? 12 ? 20 ? 20\n" - + "jail-with-long-name ? polling ? 0 ? 0 ? 0 ? 0\n" - + "????????????????????????????????????????????????????????" + + " | | Filter | Actions \n" + + " Jail | Backend |-----------x-----------\n" + + " | | cur | tot | cur | tot\n" + + "---------------------x-----------x-----------x-----------\n" + + " ssh | systemd | 3 | 6 | 12 | 24\n" + + " exim4 | pyinotify | 6 | 12 | 20 | 20\n" + + " jail-with-long-name | polling | 0 | 0 | 0 | 0\n" + + "---------------------------------------------------------" ) - response = self.b.beautify(response).encode('ascii', 'replace').decode('ascii') + response = self.b.beautify(response) self.assertEqual(response, output) diff --git a/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf b/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf index ad8adeb6..14256ba6 100644 --- a/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf +++ b/fail2ban/tests/config/filter.d/zzz-sshd-obsolete-multiline.conf @@ -9,7 +9,7 @@ before = ../../../../config/filter.d/common.conf [DEFAULT] -_daemon = sshd +_daemon = sshd(?:-session)? # optional prefix (logged from several ssh versions) like "error: ", "error: PAM: " or "fatal: " __pref = (?:(?:error|fatal): (?:PAM: )?)? diff --git a/fail2ban/tests/files/logs/apache-overflows b/fail2ban/tests/files/logs/apache-overflows index 4be013eb..31fbe478 100644 --- a/fail2ban/tests/files/logs/apache-overflows +++ b/fail2ban/tests/files/logs/apache-overflows @@ -25,3 +25,6 @@ # https://issues.apache.org/bugzilla/show_bug.cgi?id=46123 # failJSON: { "time": "2008-10-29T11:55:14", "match": true , "host": "127.0.0.1" } [Wed Oct 29 11:55:14 2008] [error] [client 127.0.0.1] Invalid method in request \x16\x03\x01 - possible attempt to establish SSL connection when the server isn't expecting it + +# failJSON: { "time": "2024-06-26T05:20:26", "match": true , "host": "192.0.2.39", "desc": "AH10244: invalid URI path, gh-3778" } +[Wed Jun 26 05:20:26.182799 2024] [core:error] [pid 2928] [client 192.0.2.39:37924] AH10244: invalid URI path (/cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh) diff --git a/fail2ban/tests/files/logs/postfix b/fail2ban/tests/files/logs/postfix index bd0daf55..b8e915c1 100644 --- a/fail2ban/tests/files/logs/postfix +++ b/fail2ban/tests/files/logs/postfix @@ -70,6 +70,9 @@ Jun 12 08:58:35 xxx postfix/smtpd[13533]: improper command pipelining after AUTH # failJSON: { "time": "2005-05-05T15:51:11", "match": true , "host": "216.245.194.173", "desc": "postfix postscreen / gh-1764" } May 5 15:51:11 xxx postfix/postscreen[1148]: NOQUEUE: reject: RCPT from [216.245.194.173]:60591: 550 5.7.1 Service unavailable; client [216.245.194.173] blocked using rbl.example.com; from=, to=, proto=ESMTP, helo= +# failJSON: { "time": "2005-06-01T19:00:55", "match": true , "host": "192.0.2.114", "desc": "postfix client restriction / gh-3800" } +Jun 1 19:00:55 mail postfix/smtpd[7749]: NOQUEUE: reject: CONNECT from unknown[192.0.2.114]: 450 4.7.25 Client host rejected: cannot find your hostname, [178.215.236.114]; proto=SMTP + # failJSON: { "time": "2005-06-03T06:25:43", "match": true , "host": "192.0.2.11", "desc": "too many errors / gh-2439" } Jun 3 06:25:43 srv postfix/smtpd[29306]: too many errors after RCPT from example.com[192.0.2.11] @@ -148,6 +151,9 @@ Jan 14 16:18:16 xxx postfix/smtpd[14933]: warning: host[192.0.2.5]: SASL CRAM-MD # failJSON: { "time": "2005-01-14T16:18:16", "match": true , "host": "192.0.2.5", "desc": "aggressive only" } Jan 14 16:18:16 xxx postfix/smtpd[14933]: warning: host[192.0.2.5]: SASL CRAM-MD5 authentication failed: Invalid authentication mechanism +# failJSON: { "time": "2004-11-04T09:11:01", "match": true , "host": "192.0.2.152", "desc": "reason unavailable" } +Nov 4 09:11:01 mail postfix/smtpd[1234]: warning: unknown[192.0.2.152]: SASL LOGIN authentication failed: (reason unavailable), sasl_username=admin + # --------------------------------------- # Test-cases of postfix DDOS mode: # --------------------------------------- diff --git a/fail2ban/tests/files/logs/proxmox b/fail2ban/tests/files/logs/proxmox new file mode 100644 index 00000000..70580f14 --- /dev/null +++ b/fail2ban/tests/files/logs/proxmox @@ -0,0 +1,5 @@ +# failJSON: { "time": "2005-03-08T09:37:44", "match": true , "host": "192.0.2.123" } +Mar 8 09:37:44 HOSTNAME pvedaemon[12021]: authentication failure; rhost=192.0.2.123 user=root@pam msg=Authentication failure + +# failJSON: { "time": "2005-03-09T03:32:27", "match": true , "host": "192.0.2.124" } +Mar 9 03:32:27 HOSTNAME pvedaemon[8961]: authentication failure; rhost=192.0.2.124 user=jose@pve msg=invalid credentials diff --git a/fail2ban/tests/files/logs/roundcube-auth b/fail2ban/tests/files/logs/roundcube-auth index f3f762d2..c596de78 100644 --- a/fail2ban/tests/files/logs/roundcube-auth +++ b/fail2ban/tests/files/logs/roundcube-auth @@ -54,3 +54,8 @@ Jul 11 03:06:37 myhostname roundcube: IMAP Error: Login failed for admin from 12 # failJSON: { "time": "2005-05-19T06:07:48", "match": true , "host": "192.0.2.1", "desc": "Roundcube logged to journald instead to a local file."} May 19 06:07:48 server roundcube[21296]: IMAP Error: Login failed for test from 192.0.2.1. AUTHENTICATE PLAIN: Authentication failed. in /usr/share/php5/Roundcube/rcube_imap.php on line 193 (POST /mail/?_task=login&_action=login) + +# Roundcube 1.5.0 (/var/log/roundcubemail/errors) +# failJSON: { "time": "2014-12-30T19:02:34", "match": true , "host": "1.2.3.4" } +[30-Dec-2014 13:02:34 -0500]: <3z506z6r> IMAP Error: Login failed for admin@example.com against localhost from 1.2.3.4. AUTHENTICATE PLAIN: Authentication failed. in /docroot/path/program/lib/Roundcube/rcube_imap.php on line 221 (POST /?_task=login?_task=login&_action=login) + diff --git a/fail2ban/tests/files/logs/sshd b/fail2ban/tests/files/logs/sshd index ed54ded4..7d3948ed 100644 --- a/fail2ban/tests/files/logs/sshd +++ b/fail2ban/tests/files/logs/sshd @@ -20,6 +20,9 @@ Feb 25 14:34:10 belka sshd[31603]: Failed password for invalid user ROOT from aa # failJSON: { "time": "2005-02-25T14:34:11", "match": true , "host": "aaaa:bbbb:cccc:1234::1:1" } Feb 25 14:34:11 belka sshd[31603]: Failed password for invalid user ROOT from aaaa:bbbb:cccc:1234::1:1 +# failJSON: { "time": "2005-07-03T14:59:17", "match": true , "host": "192.0.2.1", "desc": "new log with session in daemon prefix, gh-3782" } +Jul 3 14:59:17 host sshd-session[1571]: Failed password for root from 192.0.2.1 port 56502 ssh2 + #3 # failJSON: { "time": "2005-01-05T01:31:41", "match": true , "host": "1.2.3.4" } Jan 5 01:31:41 www sshd[1643]: ROOT LOGIN REFUSED FROM 1.2.3.4 diff --git a/fail2ban/tests/misctestcase.py b/fail2ban/tests/misctestcase.py index 1776028d..bfce434f 100644 --- a/fail2ban/tests/misctestcase.py +++ b/fail2ban/tests/misctestcase.py @@ -120,7 +120,7 @@ class SetupTest(unittest.TestCase): # suppress stdout (and stderr) if not heavydebug supdbgout = ' >/dev/null' if unittest.F2B.log_level >= logging.DEBUG else '' # HEAVYDEBUG try: - self.assertEqual(os.system("%s %s install --root=%s%s" + self.assertEqual(os.system("%s -W 'ignore:setup.py install is deprecated' %s install --root=%s%s" % (sys.executable, self.setup, tmp, supdbgout)), 0) def strippath(l): diff --git a/fail2ban/tests/servertestcase.py b/fail2ban/tests/servertestcase.py index f8cd8db5..b3bace78 100644 --- a/fail2ban/tests/servertestcase.py +++ b/fail2ban/tests/servertestcase.py @@ -2034,32 +2034,32 @@ class ServerConfigReaderTests(LogCaptureTestCase): ('j-fwcmd-rr', 'firewallcmd-rich-rules[port="22:24", protocol="tcp"]', { 'ip4': ("family='ipv4'", "icmp-port-unreachable",), 'ip6': ("family='ipv6'", 'icmp6-port-unreachable',), 'ip4-ban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family='ipv4' source address='192.0.2.1' port port='$p' protocol='tcp' reject type='icmp-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family=\"ipv4\" source address=\"192.0.2.1\" port port=\"$p\" protocol=\"tcp\" reject type='icmp-port-unreachable'"; done`""", ), 'ip4-unban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family='ipv4' source address='192.0.2.1' port port='$p' protocol='tcp' reject type='icmp-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family=\"ipv4\" source address=\"192.0.2.1\" port port=\"$p\" protocol=\"tcp\" reject type='icmp-port-unreachable'"; done`""", ), 'ip6-ban': ( - """ `ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family='ipv6' source address='2001:db8::' port port='$p' protocol='tcp' reject type='icmp6-port-unreachable'"; done`""", + r""" `ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family=\"ipv6\" source address=\"2001:db8::\" port port=\"$p\" protocol=\"tcp\" reject type='icmp6-port-unreachable'"; done`""", ), 'ip6-unban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family='ipv6' source address='2001:db8::' port port='$p' protocol='tcp' reject type='icmp6-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family=\"ipv6\" source address=\"2001:db8::\" port port=\"$p\" protocol=\"tcp\" reject type='icmp6-port-unreachable'"; done`""", ), }), # firewallcmd-rich-logging -- ('j-fwcmd-rl', 'firewallcmd-rich-logging[port="22:24", protocol="tcp"]', { 'ip4': ("family='ipv4'", "icmp-port-unreachable",), 'ip6': ("family='ipv6'", 'icmp6-port-unreachable',), 'ip4-ban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family='ipv4' source address='192.0.2.1' port port='$p' protocol='tcp' log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family=\"ipv4\" source address=\"192.0.2.1\" port port=\"$p\" protocol=\"tcp\" log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp-port-unreachable'"; done`""", ), 'ip4-unban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family='ipv4' source address='192.0.2.1' port port='$p' protocol='tcp' log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family=\"ipv4\" source address=\"192.0.2.1\" port port=\"$p\" protocol=\"tcp\" log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp-port-unreachable'"; done`""", ), 'ip6-ban': ( - """ `ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family='ipv6' source address='2001:db8::' port port='$p' protocol='tcp' log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp6-port-unreachable'"; done`""", + r""" `ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="rule family=\"ipv6\" source address=\"2001:db8::\" port port=\"$p\" protocol=\"tcp\" log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp6-port-unreachable'"; done`""", ), 'ip6-unban': ( - """`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family='ipv6' source address='2001:db8::' port port='$p' protocol='tcp' log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp6-port-unreachable'"; done`""", + r"""`ports="22:24"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="rule family=\"ipv6\" source address=\"2001:db8::\" port port=\"$p\" protocol=\"tcp\" log prefix='f2b-j-fwcmd-rl' level='info' limit value='1/m' reject type='icmp6-port-unreachable'"; done`""", ), }), ) diff --git a/fail2ban/version.py b/fail2ban/version.py index 512544a5..34dff834 100644 --- a/fail2ban/version.py +++ b/fail2ban/version.py @@ -24,7 +24,7 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko, Steven Hiscocks, Daniel Black" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2005-2016 Yaroslav Halchenko, 2013-2014 Steven Hiscocks, Daniel Black" __license__ = "GPL-v2+" -version = "1.1.0" +version = "1.1.1.dev1" def normVersion(): """ Returns fail2ban version in normalized machine-readable format""" diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index 8ed1c235..05ed1cbd 100644 --- a/man/fail2ban-client.1 +++ b/man/fail2ban-client.1 @@ -1,12 +1,12 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH FAIL2BAN-CLIENT "1" "April 2024" "Fail2Ban v1.1.0" "User Commands" +.TH FAIL2BAN-CLIENT "1" "April 2024" "Fail2Ban v1.1.1.dev1" "User Commands" .SH NAME fail2ban-client \- configure and control the server .SH SYNOPSIS .B fail2ban-client [\fI\,OPTIONS\/\fR] \fI\,\/\fR .SH DESCRIPTION -Fail2Ban v1.1.0 reads log file that contains password failure report +Fail2Ban v1.1.1.dev1 reads log file that contains password failure report and bans the corresponding IP addresses using firewall rules. .SH OPTIONS .TP diff --git a/man/fail2ban-python.1 b/man/fail2ban-python.1 index 9d1dfd0b..e673d7a0 100644 --- a/man/fail2ban-python.1 +++ b/man/fail2ban-python.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH FAIL2BAN-PYTHON "1" "April 2024" "fail2ban-python 1.1.0" "User Commands" +.TH FAIL2BAN-PYTHON "1" "April 2024" "fail2ban-python 1.1.1.1" "User Commands" .SH NAME fail2ban-python \- a helper for Fail2Ban to assure that the same Python is used .SH DESCRIPTION diff --git a/man/fail2ban-regex.1 b/man/fail2ban-regex.1 index 6dd498ed..a2f02de1 100644 --- a/man/fail2ban-regex.1 +++ b/man/fail2ban-regex.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH FAIL2BAN-REGEX "1" "April 2024" "fail2ban-regex 1.1.0" "User Commands" +.TH FAIL2BAN-REGEX "1" "April 2024" "fail2ban-regex 1.1.1.dev1" "User Commands" .SH NAME fail2ban-regex \- test Fail2ban "failregex" option .SH SYNOPSIS diff --git a/man/fail2ban-server.1 b/man/fail2ban-server.1 index 4ebcd6ed..df85419b 100644 --- a/man/fail2ban-server.1 +++ b/man/fail2ban-server.1 @@ -1,12 +1,12 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH FAIL2BAN-SERVER "1" "April 2024" "Fail2Ban v1.1.0" "User Commands" +.TH FAIL2BAN-SERVER "1" "April 2024" "Fail2Ban v1.1.1.dev1" "User Commands" .SH NAME fail2ban-server \- start the server .SH SYNOPSIS .B fail2ban-server [\fI\,OPTIONS\/\fR] .SH DESCRIPTION -Fail2Ban v1.1.0 reads log file that contains password failure report +Fail2Ban v1.1.1.dev1 reads log file that contains password failure report and bans the corresponding IP addresses using firewall rules. .SH OPTIONS .TP diff --git a/man/fail2ban-testcases.1 b/man/fail2ban-testcases.1 index b4869388..8d8dcf6e 100644 --- a/man/fail2ban-testcases.1 +++ b/man/fail2ban-testcases.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH FAIL2BAN-TESTCASES "1" "April 2024" "fail2ban-testcases 1.1.0" "User Commands" +.TH FAIL2BAN-TESTCASES "1" "April 2024" "fail2ban-testcases 1.1.1.dev1" "User Commands" .SH NAME fail2ban-testcases \- run Fail2Ban unit-tests .SH SYNOPSIS diff --git a/setup.py b/setup.py index 9f7bd8fb..ee9ea4df 100755 --- a/setup.py +++ b/setup.py @@ -24,23 +24,10 @@ __license__ = "GPL" import platform -try: - import setuptools - from setuptools import setup - from setuptools.command.install import install - from setuptools.command.install_scripts import install_scripts - from setuptools.command.build_py import build_py - build_scripts = None -except ImportError: - setuptools = None - from distutils.core import setup - -# older versions -if setuptools is None: - from distutils.command.build_py import build_py - from distutils.command.build_scripts import build_scripts - from distutils.command.install import install - from distutils.command.install_scripts import install_scripts +import setuptools +from setuptools import setup +from setuptools.command.install import install +from setuptools.command.install_scripts import install_scripts import os from os.path import isfile, join, isdir, realpath @@ -207,9 +194,9 @@ setup( url = "http://www.fail2ban.org", license = "GPL", platforms = "Posix", - cmdclass = dict({'build_py': build_py, 'build_scripts': build_scripts} if build_scripts else {}, **{ + cmdclass = { 'install_scripts': install_scripts_f2b, 'install': install_command_f2b - }), + }, scripts = [ 'bin/fail2ban-client', 'bin/fail2ban-server',