diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 76c0d8d0..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.6', 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: diff --git a/ChangeLog b/ChangeLog index 66539a37..ec52d5ba 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,8 +18,16 @@ ver. 1.1.1-dev-1 (20??/??/??) - development nightly edition * `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/recidive.conf` - restore possibility to set jail name in the filter, _jailname is positive now (gh-3769) +* `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) ver. 1.1.0 (2024/04/25) - object-found--norad-59479-cospar-2024-069a--altitude-36267km 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/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/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/sshd.conf b/config/filter.d/sshd.conf index a954774c..3a84b1ba 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: )?)? @@ -126,7 +126,7 @@ ignoreregex = maxlines = 1 -journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd +journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshd # DEV Notes: # 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/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/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):