From 1fb7ffe759f5d4ff73a6d271c5b98c8c18a0ad1c Mon Sep 17 00:00:00 2001 From: Michele Bologna Date: Fri, 14 Sep 2018 22:12:52 +0200 Subject: [PATCH 001/155] Feat: ban nginx forbidden accesses If you have configured nginx to forbid some paths in your webserver, e.g.: location ~ /\. { deny all; } if a client tries to access https://yoursite/.user.ini then you will see in nginx error log: 2018/09/14 19:03:05 [error] 2035#2035: *9134 access forbidden by rule, client: 10.20.30.40, server: www.example.net, request: "GET /.user.ini HTTP/1.1", host: "www.example.net", referrer: "https://www.example.net" By carefully setting this filter we ban every IP that tries too many times to access forbidden resources. Author: Michele Bologna https://www.michelebologna.net/ --- config/filter.d/nginx-forbidden.conf | 21 +++++++++++++++++++++ fail2ban/tests/files/logs/nginx-forbidden | 5 +++++ 2 files changed, 26 insertions(+) create mode 100644 config/filter.d/nginx-forbidden.conf create mode 100644 fail2ban/tests/files/logs/nginx-forbidden diff --git a/config/filter.d/nginx-forbidden.conf b/config/filter.d/nginx-forbidden.conf new file mode 100644 index 00000000..3c54e61e --- /dev/null +++ b/config/filter.d/nginx-forbidden.conf @@ -0,0 +1,21 @@ +# fail2ban filter configuration for nginx forbidden accesses +# +# If you have configured nginx to forbid some paths in your webserver, e.g.: +# +# location ~ /\. { +# deny all; +# } +# +# if a client tries to access https://yoursite/.user.ini then you will see +# in nginx error log: +# +# 2018/09/14 19:03:05 [error] 2035#2035: *9134 access forbidden by rule, client: 10.20.30.40, server: www.example.net, request: "GET /.user.ini HTTP/1.1", host: "www.example.net", referrer: "https://www.example.net" +# +# By carefully setting this filter we ban every IP that tries too many times to +# access forbidden resources. +# +# Author: Michele Bologna https://www.michelebologna.net/ + +[Definition] +failregex = \[error\] \d+#\d+: \*\d+ access forbidden by rule, client: +ignoreregex = diff --git a/fail2ban/tests/files/logs/nginx-forbidden b/fail2ban/tests/files/logs/nginx-forbidden new file mode 100644 index 00000000..6da3ed01 --- /dev/null +++ b/fail2ban/tests/files/logs/nginx-forbidden @@ -0,0 +1,5 @@ +# failJSON: { "time": "2018-09-14T19:03:05", "match": true , "host": "12.34.56.78" } +2018/09/14 19:03:05 [error] 2035#2035: *9134 access forbidden by rule, client: 12.34.56.78, server: www.example.net, request: "GET /wp-content/themes/evolve/js/back-end/libraries/fileuploader/upload_handler.php HTTP/1.1", host: "www.example.net", referrer: "http://example.net/foo.php" + +# failJSON: { "time": "2018-09-13T15:42:05", "match": true , "host": "12.34.56.78" } +2018/09/13 15:42:05 [error] 2035#2035: *287 access forbidden by rule, client: 12.34.56.78, server: www.example.com, request: "GET /wp-config.php~ HTTP/1.1", host: "www.example.com" From 7e88ae0ee66628893a283d6fed06a347f9f6673e Mon Sep 17 00:00:00 2001 From: Michele Bologna Date: Fri, 14 Sep 2018 23:08:12 +0200 Subject: [PATCH 002/155] Feat: add forbidden to jail.conf --- config/jail.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/jail.conf b/config/jail.conf index 697c81dd..a6f2ac5a 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -390,6 +390,11 @@ port = http,https logpath = %(nginx_error_log)s maxretry = 2 +[nginx-forbidden] + +port = http,https +logpath = %(nginx_error_log)s +maxretry = 10 # Ban attackers that try to use PHP's URL-fopen() functionality # through GET/POST variables. - Experimental, with more than a year From eba33d620554ede6d223188ed38677f36c89a2a9 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 14 Nov 2022 18:13:01 +0100 Subject: [PATCH 003/155] version bump --- ChangeLog | 8 ++++++++ fail2ban/version.py | 2 +- man/fail2ban-client.1 | 4 ++-- man/fail2ban-python.1 | 2 +- man/fail2ban-regex.1 | 2 +- man/fail2ban-server.1 | 4 ++-- man/fail2ban-testcases.1 | 2 +- 7 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8c3be67d..9c0198ca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -7,6 +7,14 @@ Fail2Ban: Changelog =================== +ver. 1.0.3-dev-1 (20??/??/??) - development nightly edition +----------- + +### Fixes + +### New Features and Enhancements + + ver. 1.0.2 (2022/11/09) - finally-war-game-test-tape-not-a-nuclear-alarm ----------- diff --git a/fail2ban/version.py b/fail2ban/version.py index 25ac2284..96a0760a 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.0.2" +version = "1.0.3.dev1" def normVersion(): """ Returns fail2ban version in normalized machine-readable format""" diff --git a/man/fail2ban-client.1 b/man/fail2ban-client.1 index e4d2f44c..9fa0b803 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.48.1. -.TH FAIL2BAN-CLIENT "1" "November 2022" "Fail2Ban v1.0.2" "User Commands" +.TH FAIL2BAN-CLIENT "1" "November 2022" "Fail2Ban v1.0.3.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.0.2 reads log file that contains password failure report +Fail2Ban v1.0.3.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 225c8295..f40c160f 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.48.1. -.TH FAIL2BAN-PYTHON "1" "November 2022" "fail2ban-python 1.0.2" "User Commands" +.TH FAIL2BAN-PYTHON "1" "November 2022" "fail2ban-python 1.0.3.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 5e64ef5b..40e8a1e8 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.48.1. -.TH FAIL2BAN-REGEX "1" "November 2022" "fail2ban-regex 1.0.2" "User Commands" +.TH FAIL2BAN-REGEX "1" "November 2022" "fail2ban-regex 1.0.3.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 ad1d84de..eb404dc9 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.48.1. -.TH FAIL2BAN-SERVER "1" "November 2022" "Fail2Ban v1.0.2" "User Commands" +.TH FAIL2BAN-SERVER "1" "November 2022" "Fail2Ban v1.0.3.dev1" "User Commands" .SH NAME fail2ban-server \- start the server .SH SYNOPSIS .B fail2ban-server [\fI\,OPTIONS\/\fR] .SH DESCRIPTION -Fail2Ban v1.0.2 reads log file that contains password failure report +Fail2Ban v1.0.3.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 7221c0cd..236e7bd8 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.48.1. -.TH FAIL2BAN-TESTCASES "1" "November 2022" "fail2ban-testcases 1.0.2" "User Commands" +.TH FAIL2BAN-TESTCASES "1" "November 2022" "fail2ban-testcases 1.0.3.dev1" "User Commands" .SH NAME fail2ban-testcases \- run Fail2Ban unit-tests .SH SYNOPSIS From 82506f0586d5e3365c74dbb3d51d2ab31bede2a0 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 14 Nov 2022 18:51:06 +0100 Subject: [PATCH 004/155] filter.d/selinux-ssh.conf, filter.d/selinux-common.conf: fixes #3405 (new format with GS and additional parameters, e. g. grantors) --- config/filter.d/selinux-common.conf | 2 +- config/filter.d/selinux-ssh.conf | 4 +++- fail2ban/tests/files/logs/selinux-ssh | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/filter.d/selinux-common.conf b/config/filter.d/selinux-common.conf index b3e0ae4f..ad4a77f5 100644 --- a/config/filter.d/selinux-common.conf +++ b/config/filter.d/selinux-common.conf @@ -14,7 +14,7 @@ [Definition] -failregex = ^type=%(_type)s msg=audit\(:\d+\): (user )?pid=\d+ uid=%(_uid)s auid=%(_auid)s ses=\d+ subj=%(_subj)s msg='%(_msg)s'$ +failregex = ^type=%(_type)s msg=audit\(:\d+\): (user )?pid=\d+ uid=%(_uid)s auid=%(_auid)s ses=\d+ subj=%(_subj)s msg='%(_msg)s'(?:\x1D|$) ignoreregex = diff --git a/config/filter.d/selinux-ssh.conf b/config/filter.d/selinux-ssh.conf index 6955094f..e5793c0a 100644 --- a/config/filter.d/selinux-ssh.conf +++ b/config/filter.d/selinux-ssh.conf @@ -15,7 +15,9 @@ _subj = (?:unconfined_u|system_u):system_r:sshd_t:s0-s0:c0\.c1023 _exe =/usr/sbin/sshd _terminal = ssh -_msg = op=\S+ acct=(?P<_quote_acct>"?)\S+(?P=_quote_acct) exe="%(_exe)s" hostname=(\?|(\d+\.){3}\d+) addr= terminal=%(_terminal)s res=failed +_anygrp = (?!acct=|exe=|addr=|terminal=|res=)\w+=(?:".*"|\S*) + +_msg = (?:%(_anygrp)s )*acct=(?:"[^"]+"|\S+) exe="%(_exe)s" (?:%(_anygrp)s )*addr= terminal=%(_terminal)s res=failed # DEV Notes: # diff --git a/fail2ban/tests/files/logs/selinux-ssh b/fail2ban/tests/files/logs/selinux-ssh index f9e1b828..6ba552fe 100644 --- a/fail2ban/tests/files/logs/selinux-ssh +++ b/fail2ban/tests/files/logs/selinux-ssh @@ -27,3 +27,6 @@ type=USER_AUTH msg=audit(1383116263.000:603): pid=12887 uid=0 auid=4294967295 se # failJSON: { "time": "2013-10-30T07:54:08", "match": false , "host": "192.168.3.100" } type=USER_LOGIN msg=audit(1383116048.000:595): pid=12354 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=login acct="dan" exe="/usr/sbin/sshd" hostname=? addr=192.168.3.100 terminal=ssh res=failed' + +# failJSON: { "time": "2022-11-14T00:11:11", "match": true , "host": "192.0.2.111" } +type=USER_AUTH msg=audit(1668381071.000:373474): pid=173582 uid=0 auid=4294967295 ses=4294967295 subj=system_u:system_r:sshd_t:s0-s0:c0.c1023 msg='op=PAM:authentication grantors=? acct="root" exe="/usr/sbin/sshd" hostname=192.0.2.111 addr=192.0.2.111 terminal=ssh res=failed'UID="root" AUID="unset" From cbb097a2b35260c516ede8620efa4e8317d9ce1c Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 14 Nov 2022 18:56:01 +0100 Subject: [PATCH 005/155] small amend (non capturing group) --- config/filter.d/selinux-common.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/filter.d/selinux-common.conf b/config/filter.d/selinux-common.conf index ad4a77f5..dc9616d2 100644 --- a/config/filter.d/selinux-common.conf +++ b/config/filter.d/selinux-common.conf @@ -14,7 +14,7 @@ [Definition] -failregex = ^type=%(_type)s msg=audit\(:\d+\): (user )?pid=\d+ uid=%(_uid)s auid=%(_auid)s ses=\d+ subj=%(_subj)s msg='%(_msg)s'(?:\x1D|$) +failregex = ^type=%(_type)s msg=audit\(:\d+\): (?:user )?pid=\d+ uid=%(_uid)s auid=%(_auid)s ses=\d+ subj=%(_subj)s msg='%(_msg)s'(?:\x1D|$) ignoreregex = From a58fcb87867f1ec83b98da224872041244f69843 Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 14 Nov 2022 19:00:08 +0100 Subject: [PATCH 006/155] fix cut out of match for pattern with `{EPOCH}` (similar to other datepatterns group capturing whole regex only added if no groups specified at all); allows to specify more precise anchored patterns, for example `datepattern = ^type=\S+ msg=audit\(({EPOCH})` for selinux-filters --- fail2ban/server/datetemplate.py | 6 ++++-- fail2ban/tests/datedetectortestcase.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/fail2ban/server/datetemplate.py b/fail2ban/server/datetemplate.py index e02772d8..518805bb 100644 --- a/fail2ban/server/datetemplate.py +++ b/fail2ban/server/datetemplate.py @@ -227,8 +227,10 @@ class DateEpoch(DateTemplate): self.name = "LongEpoch" if not pattern else pattern epochRE = r"\d{10,11}(?:\d{3}(?:\.\d{1,6}|\d{3})?)?" if pattern: - # pattern should capture/cut out the whole match: - regex = "(" + RE_EPOCH_PATTERN.sub(lambda v: "(%s)" % epochRE, pattern) + ")" + # pattern should find the whole pattern, but cut out grouped match (or whole match if no groups specified): + regex = RE_EPOCH_PATTERN.sub(lambda v: "(%s)" % epochRE, pattern) + if not RE_GROUPED.search(pattern): + regex = "(" + regex + ")" self._grpIdx = 2 self.setRegex(regex) elif not lineBeginOnly: diff --git a/fail2ban/tests/datedetectortestcase.py b/fail2ban/tests/datedetectortestcase.py index 83dd2671..bc33bc05 100644 --- a/fail2ban/tests/datedetectortestcase.py +++ b/fail2ban/tests/datedetectortestcase.py @@ -119,6 +119,15 @@ class DateDetectorTest(LogCaptureTestCase): log = log % dateLong datelog = self.datedetector.getTime(log) self.assertFalse(datelog) + + def testGetEpochPatternCut(self): + self.__datedetector = DateDetector() + self.__datedetector.appendTemplate(r'^type=\S+ msg=audit\(({EPOCH})') + # correct epoch time and cut out epoch string only (captured group only, not the whole match): + line = "type=USER_AUTH msg=audit(1106513999.000:987)" + datelog = self.datedetector.getTime(line) + timeMatch = datelog[1] + self.assertEqual([int(datelog[0]), line[timeMatch.start(1):timeMatch.end(1)]], [1106513999, '1106513999.000']) def testGetTime(self): log = "Jan 23 21:59:59 [sshd] error: PAM: Authentication failure" From ae5fe2e0032b8055a6a3c707f4cabfdd283f4245 Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Tue, 15 Nov 2022 14:29:59 +0100 Subject: [PATCH 007/155] amend to #3405, eliminate catch-all --- config/filter.d/selinux-ssh.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/filter.d/selinux-ssh.conf b/config/filter.d/selinux-ssh.conf index e5793c0a..0e38eb11 100644 --- a/config/filter.d/selinux-ssh.conf +++ b/config/filter.d/selinux-ssh.conf @@ -15,7 +15,7 @@ _subj = (?:unconfined_u|system_u):system_r:sshd_t:s0-s0:c0\.c1023 _exe =/usr/sbin/sshd _terminal = ssh -_anygrp = (?!acct=|exe=|addr=|terminal=|res=)\w+=(?:".*"|\S*) +_anygrp = (?!acct=|exe=|addr=|terminal=|res=)\w+=(?:"[^"]+"|\S*) _msg = (?:%(_anygrp)s )*acct=(?:"[^"]+"|\S+) exe="%(_exe)s" (?:%(_anygrp)s )*addr= terminal=%(_terminal)s res=failed From 05c162ef102026450244b41a6806e1137f340aba Mon Sep 17 00:00:00 2001 From: Andrey Alekseenko Date: Sat, 14 Apr 2018 17:01:36 +0300 Subject: [PATCH 008/155] Create filter for Dante SOCKS server --- config/filter.d/dante.conf | 16 ++++++++++++++++ config/jail.conf | 5 +++++ fail2ban/tests/files/logs/dante | 4 ++++ 3 files changed, 25 insertions(+) create mode 100644 config/filter.d/dante.conf create mode 100644 fail2ban/tests/files/logs/dante diff --git a/config/filter.d/dante.conf b/config/filter.d/dante.conf new file mode 100644 index 00000000..b597d461 --- /dev/null +++ b/config/filter.d/dante.conf @@ -0,0 +1,16 @@ +# Fail2Ban filter for dante +# +# Make sure you have "log: error" set in your "client pass" directive +# + +[INCLUDES] +before = common.conf + +[Definition] +_daemon = danted + +failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes in \d+ seconds: (could not access user "\w+"'s records in the system password file: no system error|system password authentication failed for user "\w+")$ + +[Init] +journalmatch = _SYSTEMD_UNIT=danted.service + diff --git a/config/jail.conf b/config/jail.conf index fe8db527..f4990e09 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -978,3 +978,8 @@ banaction = %(banaction_allports)s [monitorix] port = 8080 logpath = /var/log/monitorix-httpd + +[dante] +port = 1080 +logpath = %(syslog_daemon)s + diff --git a/fail2ban/tests/files/logs/dante b/fail2ban/tests/files/logs/dante new file mode 100644 index 00000000..a7f08eb2 --- /dev/null +++ b/fail2ban/tests/files/logs/dante @@ -0,0 +1,4 @@ +# failJSON: { "time": "2005-04-14T15:35:03", "match": true , "host": "1.2.3.4" } +Apr 14 15:35:03 vps111111 danted[17969]: info: block(1): tcp/accept ]: 1.2.3.4.50550 0.0.0.0.1080: error after reading 35 bytes in 0 seconds: could not access user "roooooooot"'s records in the system password file: no system error +# failJSON: { "time": "2005-04-14T15:44:26", "match": true , "host": "1.2.3.4" } +Apr 14 15:44:26 vps111111 danted[1846]: info: block(1): tcp/accept ]: 1.2.3.4.57178 0.0.0.0.1080: error after reading 18 bytes in 0 seconds: system password authentication failed for user "aland" From df91b047d2104e0dd26636d2cea33b480538c919 Mon Sep 17 00:00:00 2001 From: Andrey Alekseenko Date: Mon, 13 Aug 2018 20:22:37 +0300 Subject: [PATCH 009/155] Dante SOCKS server: handle "1 byte/second" case Thanks to @Loriowar and @sebres for pointing it out --- config/filter.d/dante.conf | 2 +- fail2ban/tests/files/logs/dante | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/filter.d/dante.conf b/config/filter.d/dante.conf index b597d461..d95f96b4 100644 --- a/config/filter.d/dante.conf +++ b/config/filter.d/dante.conf @@ -9,7 +9,7 @@ before = common.conf [Definition] _daemon = danted -failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes in \d+ seconds: (could not access user "\w+"'s records in the system password file: no system error|system password authentication failed for user "\w+")$ +failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes? in \d+ seconds?: (could not access user "\w+"'s records in the system password file: no system error|system password authentication failed for user "\w+")$ [Init] journalmatch = _SYSTEMD_UNIT=danted.service diff --git a/fail2ban/tests/files/logs/dante b/fail2ban/tests/files/logs/dante index a7f08eb2..80d6744f 100644 --- a/fail2ban/tests/files/logs/dante +++ b/fail2ban/tests/files/logs/dante @@ -2,3 +2,5 @@ Apr 14 15:35:03 vps111111 danted[17969]: info: block(1): tcp/accept ]: 1.2.3.4.50550 0.0.0.0.1080: error after reading 35 bytes in 0 seconds: could not access user "roooooooot"'s records in the system password file: no system error # failJSON: { "time": "2005-04-14T15:44:26", "match": true , "host": "1.2.3.4" } Apr 14 15:44:26 vps111111 danted[1846]: info: block(1): tcp/accept ]: 1.2.3.4.57178 0.0.0.0.1080: error after reading 18 bytes in 0 seconds: system password authentication failed for user "aland" +# failJSON: { "time": "2005-04-14T15:44:26", "match": true , "host": "1.2.3.4" } +Apr 14 15:44:26 vps111111 danted[1846]: info: block(1): tcp/accept ]: 1.2.3.4.57178 0.0.0.0.1080: error after reading 1 byte in 1 second: system password authentication failed for user "aland" From 996553f33015547fd2872564bc212ed974b87620 Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Fri, 18 Nov 2022 12:31:11 +0100 Subject: [PATCH 010/155] review, simplify regex and capture user name --- config/filter.d/dante.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/filter.d/dante.conf b/config/filter.d/dante.conf index d95f96b4..986f1946 100644 --- a/config/filter.d/dante.conf +++ b/config/filter.d/dante.conf @@ -9,7 +9,7 @@ before = common.conf [Definition] _daemon = danted -failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes? in \d+ seconds?: (could not access user "\w+"'s records in the system password file: no system error|system password authentication failed for user "\w+")$ +failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes? in \d+ seconds?: (could not access |system password authentication failed for )user "[^"]+" [Init] journalmatch = _SYSTEMD_UNIT=danted.service From efbbcb41ea51db6722a3ed78767579c98dc2cd0a Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Fri, 18 Nov 2022 12:32:15 +0100 Subject: [PATCH 011/155] non capturing group --- config/filter.d/dante.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/filter.d/dante.conf b/config/filter.d/dante.conf index 986f1946..e3f6f7b2 100644 --- a/config/filter.d/dante.conf +++ b/config/filter.d/dante.conf @@ -9,7 +9,7 @@ before = common.conf [Definition] _daemon = danted -failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes? in \d+ seconds?: (could not access |system password authentication failed for )user "[^"]+" +failregex = ^%(__prefix_line)sinfo: block\(1\): tcp/accept \]: \.\d+ [\d.]+: error after reading \d+ bytes? in \d+ seconds?: (?:could not access |system password authentication failed for )user "[^"]+" [Init] journalmatch = _SYSTEMD_UNIT=danted.service From 432e7e1e93936f09e349e80d94254e5f43d0cc8a Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Mon, 28 Nov 2022 13:21:15 +0100 Subject: [PATCH 012/155] no warning if no config value but default (debug message now) closes #3420 --- fail2ban/client/configreader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fail2ban/client/configreader.py b/fail2ban/client/configreader.py index 1b5a56a2..c7f965ce 100644 --- a/fail2ban/client/configreader.py +++ b/fail2ban/client/configreader.py @@ -277,7 +277,7 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes): # TODO: validate error handling here. except NoOptionError: if not optvalue is None: - logSys.warning("'%s' not defined in '%s'. Using default one: %r" + logSys.debug("'%s' not defined in '%s'. Using default one: %r" % (optname, sec, optvalue)) values[optname] = optvalue # elif logSys.getEffectiveLevel() <= logLevel: From 58834b6734dff4d07e11de304d5aa7be1c14936b Mon Sep 17 00:00:00 2001 From: sebres Date: Fri, 6 Jan 2023 14:49:22 +0100 Subject: [PATCH 013/155] better auto-detection for IPv6 support (`allowipv6 = auto` by default); circumvent SF in some python's socket module by getaddrinfo with disabled IPv6 (closes gh-3438) --- fail2ban/server/ipdns.py | 42 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index d917d031..68757c25 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -92,14 +92,14 @@ class DNSUtils: # retrieve ips ips = set() saveerr = None - for fam, ipfam in ((socket.AF_INET, IPAddr.FAM_IPv4), (socket.AF_INET6, IPAddr.FAM_IPv6)): + for fam in ((socket.AF_INET,socket.AF_INET6) if DNSUtils.IPv6IsAllowed(True) else (socket.AF_INET,)): try: for result in socket.getaddrinfo(dns, None, fam, 0, socket.IPPROTO_TCP): # if getaddrinfo returns something unexpected: if len(result) < 4 or not len(result[4]): continue # get ip from `(2, 1, 6, '', ('127.0.0.1', 0))`,be sure we've an ip-string # (some python-versions resp. host configurations causes returning of integer there): - ip = IPAddr(str(result[4][0]), ipfam) + ip = IPAddr(str(result[4][0]), IPAddr._AF2FAM(fam)) if ip.isValid: ips.add(ip) except Exception as e: @@ -208,6 +208,31 @@ class DNSUtils: _IPv6IsAllowed = None + @staticmethod + def _IPv6IsSupportedBySystem(): + if not socket.has_ipv6: + return False + s = None + try: + # try to create INET6 socket: + s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + # bind it to free port for any interface supporting IPv6: + s.bind(("", 0)); + return True + except Exception as e: # pragma: no cover + if hasattr(e, 'errno'): + import errno + # negative (-9 'Address family not supported', etc) or not available/supported: + if e.errno < 0 or e.errno in (errno.EADDRNOTAVAIL, errno.EAFNOSUPPORT): + return False + # in use: + if e.errno in (errno.EADDRINUSE, errno.EACCES): # normally unreachable (free port and root) + return True + finally: + if s: s.close() + # unable to detect: + return None + @staticmethod def setIPv6IsAllowed(value): DNSUtils._IPv6IsAllowed = value @@ -218,13 +243,19 @@ class DNSUtils: _IPv6IsAllowed_key = ('self','ipv6-allowed') @staticmethod - def IPv6IsAllowed(): + def IPv6IsAllowed(knownOnly=False): if DNSUtils._IPv6IsAllowed is not None: return DNSUtils._IPv6IsAllowed v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key) if v is not None: return v - v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) + v = DNSUtils._IPv6IsSupportedBySystem() + if v is None: + # avoid self recursion (and assume we may have IPv6 during auto-detection): + if knownOnly: + return True + # detect by IPs of host: + v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v) return v @@ -255,6 +286,9 @@ class IPAddr(object): CIDR_UNSPEC = -1 FAM_IPv4 = CIDR_RAW - socket.AF_INET FAM_IPv6 = CIDR_RAW - socket.AF_INET6 + @staticmethod + def _AF2FAM(v): + return IPAddr.CIDR_RAW - v def __new__(cls, ipstr, cidr=CIDR_UNSPEC): if cidr == IPAddr.CIDR_UNSPEC and isinstance(ipstr, (tuple, list)): From d8a9812adc0c5b4accc12cd7d1d16efba02af1aa Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 9 Jan 2023 16:21:36 +0100 Subject: [PATCH 014/155] improve auto detection of IPv6 - try to check sysctl net.ipv6.conf.all.disable_ipv6 (prefer value read from `/proc/sys/net/ipv6/conf/all/disable_ipv6`) --- fail2ban/server/ipdns.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index 68757c25..aca0f6a6 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -212,6 +212,13 @@ class DNSUtils: def _IPv6IsSupportedBySystem(): if not socket.has_ipv6: return False + # try to check sysctl net.ipv6.conf.all.disable_ipv6: + try: + with open('/proc/sys/net/ipv6/conf/all/disable_ipv6', 'rb') as f: + # if 1 - disabled, 0 - enabled + return not int(f.read()) + except: + pass s = None try: # try to create INET6 socket: From 09c23fd5b8593cecc1744db3d6e0dba7de67480c Mon Sep 17 00:00:00 2001 From: sebres Date: Mon, 9 Jan 2023 21:52:12 +0100 Subject: [PATCH 015/155] try to obtain local addresses from network interfaces before DNS to IP lookup (closes gh-3132); DNSUtils.getSelfIP returns IPAddrSet now (because own IPs may be the subnets now, so the check `ignoreself` must check whether any of subnets contains the IP) --- fail2ban/server/ipdns.py | 162 ++++++++++++++++++++++++++----- fail2ban/tests/filtertestcase.py | 34 ++++++- 2 files changed, 173 insertions(+), 23 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index aca0f6a6..75e21a31 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -154,17 +154,18 @@ class DNSUtils: # try find cached own hostnames (this tuple-key cannot be used elsewhere): key = ('self','hostname', fqdn) name = DNSUtils.CACHE_ipToName.get(key) + if name is not None: + return name # get it using different ways (hostname, fully-qualified or vice versa): - if name is None: - name = '' - for hostname in ( - (getfqdn, socket.gethostname) if fqdn else (socket.gethostname, getfqdn) - ): - try: - name = hostname() - break - except Exception as e: # pragma: no cover - logSys.warning("Retrieving own hostnames failed: %s", e) + name = '' + for hostname in ( + (getfqdn, socket.gethostname) if fqdn else (socket.gethostname, getfqdn) + ): + try: + name = hostname() + break + except Exception as e: # pragma: no cover + logSys.warning("Retrieving own hostnames failed: %s", e) # cache and return : DNSUtils.CACHE_ipToName.set(key, name) return name @@ -177,11 +178,12 @@ class DNSUtils: """Get own host names of self""" # try find cached own hostnames: names = DNSUtils.CACHE_ipToName.get(DNSUtils._getSelfNames_key) + if names is not None: + return names # get it using different ways (a set with names of localhost, hostname, fully qualified): - if names is None: - names = set([ - 'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True) - ]) - set(['']) # getHostname can return '' + names = set([ + 'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True) + ]) - set(['']) # getHostname can return '' # cache and return : DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names) return names @@ -194,14 +196,19 @@ class DNSUtils: """Get own IP addresses of self""" # to find cached own IPs: ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getSelfIPs_key) - # get it using different ways (a set with IPs of localhost, hostname, fully qualified): - if ips is None: - ips = set() - for hostname in DNSUtils.getSelfNames(): - try: - ips |= set(DNSUtils.textToIp(hostname, 'yes')) - except Exception as e: # pragma: no cover - logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) + if ips is not None: + return ips + # firstly try to obtain from network interfaces if possible (implemented for this platform): + try: + ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs()]) + except: + ips = IPAddrSet() + # extend it using different ways (a set with IPs of localhost, hostname, fully qualified): + for hostname in DNSUtils.getSelfNames(): + try: + ips |= IPAddrSet(DNSUtils.textToIp(hostname, 'yes')) + except Exception as e: # pragma: no cover + logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) # cache and return : DNSUtils.CACHE_nameToIp.set(DNSUtils._getSelfIPs_key, ips) return ips @@ -586,6 +593,9 @@ class IPAddr(object): """ return isinstance(ip, IPAddr) and (ip == self or ip.isInNet(self)) + def __contains__(self, ip): + return self.contains(ip) + # Pre-calculated map: addr to maskplen def __getMaskMap(): m6 = (1 << 128)-1 @@ -635,3 +645,111 @@ class IPAddr(object): # An IPv4 compatible IPv6 to be reused IPAddr.IP6_4COMPAT = IPAddr("::ffff:0:0", 96) + + +class IPAddrSet(set): + + def __contains__(self, ip): + if not isinstance(ip, IPAddr): ip = IPAddr(ip) + # IP can be found directly or IP is in each subnet: + return set.__contains__(self, ip) or any(n.contains(ip) for n in self) + + +def _NetworkInterfacesAddrs(): + + # Closure implementing lazy load modules and libc and define _NetworkInterfacesAddrs on demand: + # Currently tested on Linux only (TODO: implement for MacOS, Solaris, etc) + + from ctypes import ( + Structure, Union, POINTER, + pointer, get_errno, cast, + c_ushort, c_byte, c_void_p, c_char_p, c_uint, c_int, c_uint16, c_uint32 + ) + import ctypes.util + import ctypes + + class struct_sockaddr(Structure): + _fields_ = [ + ('sa_family', c_ushort), + ('sa_data', c_byte * 14),] + + class struct_sockaddr_in(Structure): + _fields_ = [ + ('sin_family', c_ushort), + ('sin_port', c_uint16), + ('sin_addr', c_byte * 4)] + + class struct_sockaddr_in6(Structure): + _fields_ = [ + ('sin6_family', c_ushort), + ('sin6_port', c_uint16), + ('sin6_flowinfo', c_uint32), + ('sin6_addr', c_byte * 16), + ('sin6_scope_id', c_uint32)] + + class union_ifa_ifu(Union): + _fields_ = [ + ('ifu_broadaddr', POINTER(struct_sockaddr)), + ('ifu_dstaddr', POINTER(struct_sockaddr)),] + + class struct_ifaddrs(Structure): + pass + struct_ifaddrs._fields_ = [ + ('ifa_next', POINTER(struct_ifaddrs)), + ('ifa_name', c_char_p), + ('ifa_flags', c_uint), + ('ifa_addr', POINTER(struct_sockaddr)), + ('ifa_netmask', POINTER(struct_sockaddr)), + ('ifa_ifu', union_ifa_ifu), + ('ifa_data', c_void_p),] + + libc = ctypes.CDLL(ctypes.util.find_library('c')) + + def ifap_iter(ifap): + ifa = ifap.contents + while True: + yield ifa + if not ifa.ifa_next: + break + ifa = ifa.ifa_next.contents + + def getfamaddr(ifa): + sa = ifa.ifa_addr.contents + fam = sa.sa_family + if fam == socket.AF_INET: + sa = cast(pointer(sa), POINTER(struct_sockaddr_in)).contents + addr = socket.inet_ntop(fam, sa.sin_addr) + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin_addr) + return IPAddr(addr) + elif fam == socket.AF_INET6: + sa = cast(pointer(sa), POINTER(struct_sockaddr_in6)).contents + addr = socket.inet_ntop(fam, sa.sin6_addr) + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET6: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in6)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin6_addr) + return IPAddr(addr) + return None + + def _NetworkInterfacesAddrs(): + ifap = POINTER(struct_ifaddrs)() + result = libc.getifaddrs(pointer(ifap)) + if result != 0: + raise OSError(get_errno()) + del result + try: + for ifa in ifap_iter(ifap): + name = ifa.ifa_name.decode("UTF-8") + addr = getfamaddr(ifa) + if addr: + yield name, addr + finally: + libc.freeifaddrs(ifap) + + DNSUtils._NetworkInterfacesAddrs = staticmethod(_NetworkInterfacesAddrs); + return _NetworkInterfacesAddrs() + +DNSUtils._NetworkInterfacesAddrs = staticmethod(_NetworkInterfacesAddrs); diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 017e54ec..24f5272e 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -40,7 +40,7 @@ from ..server.jail import Jail from ..server.filterpoll import FilterPoll from ..server.filter import FailTicket, Filter, FileFilter, FileContainer from ..server.failmanager import FailManagerEmpty -from ..server.ipdns import asip, getfqdn, DNSUtils, IPAddr +from ..server.ipdns import asip, getfqdn, DNSUtils, IPAddr, IPAddrSet from ..server.mytime import MyTime from ..server.utils import Utils, uni_decode from .databasetestcase import getFail2BanDb @@ -2333,6 +2333,38 @@ class DNSUtilsNetworkTests(unittest.TestCase): ip1 = IPAddr('93.184.216.34'); ip2 = IPAddr('93.184.216.34'); self.assertEqual(id(ip1), id(ip2)) ip1 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); ip2 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); self.assertEqual(id(ip1), id(ip2)) + def test_IPAddrSet(self): + ips = IPAddrSet([IPAddr('192.0.2.1/27'), IPAddr('2001:DB8::/32')]) + self.assertTrue(IPAddr('192.0.2.1') in ips) + self.assertTrue(IPAddr('192.0.2.31') in ips) + self.assertFalse(IPAddr('192.0.2.32') in ips) + self.assertTrue(IPAddr('2001:DB8::1') in ips) + self.assertTrue(IPAddr('2001:0DB8:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF') in ips) + self.assertFalse(IPAddr('2001:DB9::') in ips) + # self IPs must be a set too (cover different mechanisms to obtain own IPs): + for cov in ('ni', 'dns', 'last'): + _org_NetworkInterfacesAddrs = None + if cov == 'dns': # mock-up _NetworkInterfacesAddrs like it's not implemented (raises error) + _org_NetworkInterfacesAddrs = DNSUtils._NetworkInterfacesAddrs + def _tmp_NetworkInterfacesAddrs(): + raise NotImplementedError(); + DNSUtils._NetworkInterfacesAddrs = staticmethod(_tmp_NetworkInterfacesAddrs) + try: + ips = DNSUtils.getSelfIPs() + # print('*****', ips) + if ips: + ip = IPAddr('127.0.0.1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + ip = IPAddr('127.0.0.2') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + ip = IPAddr('::1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + finally: + if _org_NetworkInterfacesAddrs: + DNSUtils._NetworkInterfacesAddrs = staticmethod(_org_NetworkInterfacesAddrs) + if cov != 'last': + DNSUtils.CACHE_nameToIp.unset(DNSUtils._getSelfIPs_key) + def testFQDN(self): unittest.F2B.SkipIfNoNetwork() sname = DNSUtils.getHostname(fqdn=False) From cb8674e68a4f2752132443d3be36462078339673 Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 10 Jan 2023 12:20:48 +0100 Subject: [PATCH 016/155] amend with few improvements, IPv6IsAllowed prefers IPs from network interfaces (if available for platform) and uses DNS (socket.getaddrinfo) as a fallback only --- fail2ban/server/ipdns.py | 209 +++++++++++++++++-------------- fail2ban/tests/filtertestcase.py | 14 ++- 2 files changed, 131 insertions(+), 92 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index 75e21a31..8c5c277e 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -92,7 +92,7 @@ class DNSUtils: # retrieve ips ips = set() saveerr = None - for fam in ((socket.AF_INET,socket.AF_INET6) if DNSUtils.IPv6IsAllowed(True) else (socket.AF_INET,)): + for fam in ((socket.AF_INET,socket.AF_INET6) if DNSUtils.IPv6IsAllowed() else (socket.AF_INET,)): try: for result in socket.getaddrinfo(dns, None, fam, 0, socket.IPPROTO_TCP): # if getaddrinfo returns something unexpected: @@ -188,6 +188,25 @@ class DNSUtils: DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names) return names + # key to find cached network interfaces IPs (this tuple-key cannot be used elsewhere): + _getNetIntrfIPs_key = ('netintrf','ips') + + @staticmethod + def getNetIntrfIPs(): + """Get own IP addresses of self""" + # to find cached own IPs: + ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getNetIntrfIPs_key) + if ips is not None: + return ips + # try to obtain from network interfaces if possible (implemented for this platform): + try: + ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs()]) + except: + ips = IPAddrSet() + # cache and return : + DNSUtils.CACHE_nameToIp.set(DNSUtils._getNetIntrfIPs_key, ips) + return ips + # key to find cached own IPs (this tuple-key cannot be used elsewhere): _getSelfIPs_key = ('self','ips') @@ -199,14 +218,11 @@ class DNSUtils: if ips is not None: return ips # firstly try to obtain from network interfaces if possible (implemented for this platform): - try: - ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs()]) - except: - ips = IPAddrSet() + ips = IPAddrSet(DNSUtils.getNetIntrfIPs()) # extend it using different ways (a set with IPs of localhost, hostname, fully qualified): for hostname in DNSUtils.getSelfNames(): try: - ips |= IPAddrSet(DNSUtils.textToIp(hostname, 'yes')) + ips |= IPAddrSet(DNSUtils.dnsToIp(hostname)) except Exception as e: # pragma: no cover logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) # cache and return : @@ -257,7 +273,7 @@ class DNSUtils: _IPv6IsAllowed_key = ('self','ipv6-allowed') @staticmethod - def IPv6IsAllowed(knownOnly=False): + def IPv6IsAllowed(): if DNSUtils._IPv6IsAllowed is not None: return DNSUtils._IPv6IsAllowed v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key) @@ -265,11 +281,15 @@ class DNSUtils: return v v = DNSUtils._IPv6IsSupportedBySystem() if v is None: - # avoid self recursion (and assume we may have IPv6 during auto-detection): - if knownOnly: - return True # detect by IPs of host: - v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) + ips = DNSUtils.getNetIntrfIPs() + if not ips: + DNSUtils._IPv6IsAllowed = True; # avoid self recursion from getSelfIPs -> dnsToIp -> IPv6IsAllowed + try: + ips = DNSUtils.getSelfIPs() + finally: + DNSUtils._IPv6IsAllowed = None + v = any((':' in ip.ntoa) for ip in ips) DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v) return v @@ -659,95 +679,102 @@ def _NetworkInterfacesAddrs(): # Closure implementing lazy load modules and libc and define _NetworkInterfacesAddrs on demand: # Currently tested on Linux only (TODO: implement for MacOS, Solaris, etc) + try: + from ctypes import ( + Structure, Union, POINTER, + pointer, get_errno, cast, + c_ushort, c_byte, c_void_p, c_char_p, c_uint, c_int, c_uint16, c_uint32 + ) + import ctypes.util + import ctypes - from ctypes import ( - Structure, Union, POINTER, - pointer, get_errno, cast, - c_ushort, c_byte, c_void_p, c_char_p, c_uint, c_int, c_uint16, c_uint32 - ) - import ctypes.util - import ctypes + class struct_sockaddr(Structure): + _fields_ = [ + ('sa_family', c_ushort), + ('sa_data', c_byte * 14),] - class struct_sockaddr(Structure): - _fields_ = [ - ('sa_family', c_ushort), - ('sa_data', c_byte * 14),] + class struct_sockaddr_in(Structure): + _fields_ = [ + ('sin_family', c_ushort), + ('sin_port', c_uint16), + ('sin_addr', c_byte * 4)] - class struct_sockaddr_in(Structure): - _fields_ = [ - ('sin_family', c_ushort), - ('sin_port', c_uint16), - ('sin_addr', c_byte * 4)] + class struct_sockaddr_in6(Structure): + _fields_ = [ + ('sin6_family', c_ushort), + ('sin6_port', c_uint16), + ('sin6_flowinfo', c_uint32), + ('sin6_addr', c_byte * 16), + ('sin6_scope_id', c_uint32)] - class struct_sockaddr_in6(Structure): - _fields_ = [ - ('sin6_family', c_ushort), - ('sin6_port', c_uint16), - ('sin6_flowinfo', c_uint32), - ('sin6_addr', c_byte * 16), - ('sin6_scope_id', c_uint32)] + class union_ifa_ifu(Union): + _fields_ = [ + ('ifu_broadaddr', POINTER(struct_sockaddr)), + ('ifu_dstaddr', POINTER(struct_sockaddr)),] - class union_ifa_ifu(Union): - _fields_ = [ - ('ifu_broadaddr', POINTER(struct_sockaddr)), - ('ifu_dstaddr', POINTER(struct_sockaddr)),] + class struct_ifaddrs(Structure): + pass + struct_ifaddrs._fields_ = [ + ('ifa_next', POINTER(struct_ifaddrs)), + ('ifa_name', c_char_p), + ('ifa_flags', c_uint), + ('ifa_addr', POINTER(struct_sockaddr)), + ('ifa_netmask', POINTER(struct_sockaddr)), + ('ifa_ifu', union_ifa_ifu), + ('ifa_data', c_void_p),] - class struct_ifaddrs(Structure): - pass - struct_ifaddrs._fields_ = [ - ('ifa_next', POINTER(struct_ifaddrs)), - ('ifa_name', c_char_p), - ('ifa_flags', c_uint), - ('ifa_addr', POINTER(struct_sockaddr)), - ('ifa_netmask', POINTER(struct_sockaddr)), - ('ifa_ifu', union_ifa_ifu), - ('ifa_data', c_void_p),] + libc = ctypes.CDLL(ctypes.util.find_library('c') or "") + if not libc.getifaddrs: # pragma: no cover + raise NotImplementedError('libc.getifaddrs is not available') - libc = ctypes.CDLL(ctypes.util.find_library('c')) + def ifap_iter(ifap): + ifa = ifap.contents + while True: + yield ifa + if not ifa.ifa_next: + break + ifa = ifa.ifa_next.contents - def ifap_iter(ifap): - ifa = ifap.contents - while True: - yield ifa - if not ifa.ifa_next: - break - ifa = ifa.ifa_next.contents + def getfamaddr(ifa): + sa = ifa.ifa_addr.contents + fam = sa.sa_family + if fam == socket.AF_INET: + sa = cast(pointer(sa), POINTER(struct_sockaddr_in)).contents + addr = socket.inet_ntop(fam, sa.sin_addr) + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin_addr) + return IPAddr(addr) + elif fam == socket.AF_INET6: + sa = cast(pointer(sa), POINTER(struct_sockaddr_in6)).contents + addr = socket.inet_ntop(fam, sa.sin6_addr) + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET6: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in6)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin6_addr) + return IPAddr(addr) + return None - def getfamaddr(ifa): - sa = ifa.ifa_addr.contents - fam = sa.sa_family - if fam == socket.AF_INET: - sa = cast(pointer(sa), POINTER(struct_sockaddr_in)).contents - addr = socket.inet_ntop(fam, sa.sin_addr) - nm = ifa.ifa_netmask.contents - if nm is not None and nm.sa_family == socket.AF_INET: - nm = cast(pointer(nm), POINTER(struct_sockaddr_in)).contents - addr += '/'+socket.inet_ntop(fam, nm.sin_addr) - return IPAddr(addr) - elif fam == socket.AF_INET6: - sa = cast(pointer(sa), POINTER(struct_sockaddr_in6)).contents - addr = socket.inet_ntop(fam, sa.sin6_addr) - nm = ifa.ifa_netmask.contents - if nm is not None and nm.sa_family == socket.AF_INET6: - nm = cast(pointer(nm), POINTER(struct_sockaddr_in6)).contents - addr += '/'+socket.inet_ntop(fam, nm.sin6_addr) - return IPAddr(addr) - return None - - def _NetworkInterfacesAddrs(): - ifap = POINTER(struct_ifaddrs)() - result = libc.getifaddrs(pointer(ifap)) - if result != 0: - raise OSError(get_errno()) - del result - try: - for ifa in ifap_iter(ifap): - name = ifa.ifa_name.decode("UTF-8") - addr = getfamaddr(ifa) - if addr: - yield name, addr - finally: - libc.freeifaddrs(ifap) + def _NetworkInterfacesAddrs(): + ifap = POINTER(struct_ifaddrs)() + result = libc.getifaddrs(pointer(ifap)) + if result != 0: + raise OSError(get_errno()) + del result + try: + for ifa in ifap_iter(ifap): + name = ifa.ifa_name.decode("UTF-8") + addr = getfamaddr(ifa) + if addr: + yield name, addr + finally: + libc.freeifaddrs(ifap) + + except Exception as e: # pragma: no cover + _init_error = NotImplementedError(e) + def _NetworkInterfacesAddrs(): + raise _init_error DNSUtils._NetworkInterfacesAddrs = staticmethod(_NetworkInterfacesAddrs); return _NetworkInterfacesAddrs() diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 24f5272e..9f96c190 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -2333,6 +2333,17 @@ class DNSUtilsNetworkTests(unittest.TestCase): ip1 = IPAddr('93.184.216.34'); ip2 = IPAddr('93.184.216.34'); self.assertEqual(id(ip1), id(ip2)) ip1 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); ip2 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); self.assertEqual(id(ip1), id(ip2)) + def test_NetworkInterfacesAddrs(self): + try: + ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs()]) + ip = IPAddr('127.0.0.1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + ip = IPAddr('::1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + except Exception as e: # pragma: no cover + # simply skip if not available, TODO: make coverage platform dependent + raise unittest.SkipTest(e) + def test_IPAddrSet(self): ips = IPAddrSet([IPAddr('192.0.2.1/27'), IPAddr('2001:DB8::/32')]) self.assertTrue(IPAddr('192.0.2.1') in ips) @@ -2347,7 +2358,7 @@ class DNSUtilsNetworkTests(unittest.TestCase): if cov == 'dns': # mock-up _NetworkInterfacesAddrs like it's not implemented (raises error) _org_NetworkInterfacesAddrs = DNSUtils._NetworkInterfacesAddrs def _tmp_NetworkInterfacesAddrs(): - raise NotImplementedError(); + raise NotImplementedError() DNSUtils._NetworkInterfacesAddrs = staticmethod(_tmp_NetworkInterfacesAddrs) try: ips = DNSUtils.getSelfIPs() @@ -2364,6 +2375,7 @@ class DNSUtilsNetworkTests(unittest.TestCase): DNSUtils._NetworkInterfacesAddrs = staticmethod(_org_NetworkInterfacesAddrs) if cov != 'last': DNSUtils.CACHE_nameToIp.unset(DNSUtils._getSelfIPs_key) + DNSUtils.CACHE_nameToIp.unset(DNSUtils._getNetIntrfIPs_key) def testFQDN(self): unittest.F2B.SkipIfNoNetwork() From 582436aadf310ff2db4d0dc76668c5fa7e5a3c03 Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 11 Jan 2023 18:27:44 +0100 Subject: [PATCH 017/155] don't add subnets to local addresses of `ignoreself` from network interfaces, use only IPs instead (subnets may be too heavy and not wanted, todo: make it configurable later) --- fail2ban/server/ipdns.py | 45 ++++++++++++++++++++++---------- fail2ban/tests/filtertestcase.py | 19 +++++++------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/fail2ban/server/ipdns.py b/fail2ban/server/ipdns.py index 8c5c277e..b435c6df 100644 --- a/fail2ban/server/ipdns.py +++ b/fail2ban/server/ipdns.py @@ -669,13 +669,28 @@ IPAddr.IP6_4COMPAT = IPAddr("::ffff:0:0", 96) class IPAddrSet(set): + hasSubNet = False + + def __init__(self, ips=[]): + ips2 = set() + for ip in ips: + if not isinstance(ip, IPAddr): ip = IPAddr(ip) + ips2.add(ip) + self.hasSubNet |= not ip.isSingle + set.__init__(self, ips2) + + def add(self, ip): + if not isinstance(ip, IPAddr): ip = IPAddr(ip) + self.hasSubNet |= not ip.isSingle + set.add(self, ip) + def __contains__(self, ip): if not isinstance(ip, IPAddr): ip = IPAddr(ip) # IP can be found directly or IP is in each subnet: - return set.__contains__(self, ip) or any(n.contains(ip) for n in self) + return set.__contains__(self, ip) or (self.hasSubNet and any(n.contains(ip) for n in self)) -def _NetworkInterfacesAddrs(): +def _NetworkInterfacesAddrs(withMask=False): # Closure implementing lazy load modules and libc and define _NetworkInterfacesAddrs on demand: # Currently tested on Linux only (TODO: implement for MacOS, Solaris, etc) @@ -735,28 +750,30 @@ def _NetworkInterfacesAddrs(): break ifa = ifa.ifa_next.contents - def getfamaddr(ifa): + def getfamaddr(ifa, withMask=False): sa = ifa.ifa_addr.contents fam = sa.sa_family if fam == socket.AF_INET: sa = cast(pointer(sa), POINTER(struct_sockaddr_in)).contents addr = socket.inet_ntop(fam, sa.sin_addr) - nm = ifa.ifa_netmask.contents - if nm is not None and nm.sa_family == socket.AF_INET: - nm = cast(pointer(nm), POINTER(struct_sockaddr_in)).contents - addr += '/'+socket.inet_ntop(fam, nm.sin_addr) + if withMask: + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin_addr) return IPAddr(addr) elif fam == socket.AF_INET6: sa = cast(pointer(sa), POINTER(struct_sockaddr_in6)).contents addr = socket.inet_ntop(fam, sa.sin6_addr) - nm = ifa.ifa_netmask.contents - if nm is not None and nm.sa_family == socket.AF_INET6: - nm = cast(pointer(nm), POINTER(struct_sockaddr_in6)).contents - addr += '/'+socket.inet_ntop(fam, nm.sin6_addr) + if withMask: + nm = ifa.ifa_netmask.contents + if nm is not None and nm.sa_family == socket.AF_INET6: + nm = cast(pointer(nm), POINTER(struct_sockaddr_in6)).contents + addr += '/'+socket.inet_ntop(fam, nm.sin6_addr) return IPAddr(addr) return None - def _NetworkInterfacesAddrs(): + def _NetworkInterfacesAddrs(withMask=False): ifap = POINTER(struct_ifaddrs)() result = libc.getifaddrs(pointer(ifap)) if result != 0: @@ -765,7 +782,7 @@ def _NetworkInterfacesAddrs(): try: for ifa in ifap_iter(ifap): name = ifa.ifa_name.decode("UTF-8") - addr = getfamaddr(ifa) + addr = getfamaddr(ifa, withMask) if addr: yield name, addr finally: @@ -777,6 +794,6 @@ def _NetworkInterfacesAddrs(): raise _init_error DNSUtils._NetworkInterfacesAddrs = staticmethod(_NetworkInterfacesAddrs); - return _NetworkInterfacesAddrs() + return _NetworkInterfacesAddrs(withMask) DNSUtils._NetworkInterfacesAddrs = staticmethod(_NetworkInterfacesAddrs); diff --git a/fail2ban/tests/filtertestcase.py b/fail2ban/tests/filtertestcase.py index 9f96c190..4e308e38 100644 --- a/fail2ban/tests/filtertestcase.py +++ b/fail2ban/tests/filtertestcase.py @@ -2334,15 +2334,16 @@ class DNSUtilsNetworkTests(unittest.TestCase): ip1 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); ip2 = IPAddr('2606:2800:220:1:248:1893:25c8:1946'); self.assertEqual(id(ip1), id(ip2)) def test_NetworkInterfacesAddrs(self): - try: - ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs()]) - ip = IPAddr('127.0.0.1') - self.assertEqual(ip in ips, any(ip in n for n in ips)) - ip = IPAddr('::1') - self.assertEqual(ip in ips, any(ip in n for n in ips)) - except Exception as e: # pragma: no cover - # simply skip if not available, TODO: make coverage platform dependent - raise unittest.SkipTest(e) + for withMask in (False, True): + try: + ips = IPAddrSet([a for ni, a in DNSUtils._NetworkInterfacesAddrs(withMask)]) + ip = IPAddr('127.0.0.1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + ip = IPAddr('::1') + self.assertEqual(ip in ips, any(ip in n for n in ips)) + except Exception as e: # pragma: no cover + # simply skip if not available, TODO: make coverage platform dependent + raise unittest.SkipTest(e) def test_IPAddrSet(self): ips = IPAddrSet([IPAddr('192.0.2.1/27'), IPAddr('2001:DB8::/32')]) From ed135b6a932bb4ab2928cc7617729ebf12a532f8 Mon Sep 17 00:00:00 2001 From: sebres Date: Wed, 11 Jan 2023 18:30:37 +0100 Subject: [PATCH 018/155] changelog entries (gh-3438, gh-3132) --- ChangeLog | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ChangeLog b/ChangeLog index 9c0198ca..a2259e36 100644 --- a/ChangeLog +++ b/ChangeLog @@ -11,8 +11,13 @@ ver. 1.0.3-dev-1 (20??/??/??) - development nightly edition ----------- ### Fixes +* circumvent SEGFAULT in a python's socket module by getaddrinfo with disabled IPv6 (gh-3438) ### New Features and Enhancements +* better auto-detection for IPv6 support (`allowipv6 = auto` by default), trying to check sysctl net.ipv6.conf.all.disable_ipv6 + (value read from `/proc/sys/net/ipv6/conf/all/disable_ipv6`) if available, otherwise seeks over local IPv6 from network interfaces + if available for platform and uses DNS to find local IPv6 as a fallback only +* improve `ignoreself` by considering all local addresses from network interfaces additionally to IPs from hostnames (gh-3132) ver. 1.0.2 (2022/11/09) - finally-war-game-test-tape-not-a-nuclear-alarm From f93a538693f25c5e735cf88a0f2fc942b943d18b Mon Sep 17 00:00:00 2001 From: sebres Date: Tue, 17 Jan 2023 12:53:39 +0100 Subject: [PATCH 019/155] gh-3447: fix careless mistake arisen in b12a3acb06fed4f240e1cea20f4b07f913edf221 by attempt to implement new reload capacity (rewritten latter): causing error "'noduplicates' is not defined" by double jail configuration --- fail2ban/server/jails.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fail2ban/server/jails.py b/fail2ban/server/jails.py index 27e12ddf..eaaa9518 100644 --- a/fail2ban/server/jails.py +++ b/fail2ban/server/jails.py @@ -67,8 +67,7 @@ class Jails(Mapping): """ with self.__lock: if name in self._jails: - if noduplicates: - raise DuplicateJailException(name) + raise DuplicateJailException(name) else: self._jails[name] = Jail(name, backend, db) From 5dcbc0dd551f711ade5b24ba5ae706d965c9fb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Turon?= <67438054+Trotyl84@users.noreply.github.com> Date: Sat, 18 Feb 2023 23:49:28 +0100 Subject: [PATCH 020/155] Update .gitignore Please add this entry for virtual python interpreter. This directory name is needed in the PyCharm environment. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 780ecfb5..5f1b8924 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ htmlcov __pycache__ .vagrant/ .idea/ +.venv/ From 92fae68071a6933428bebd5f30f7927cbf82a5fa Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Tue, 28 Feb 2023 11:32:28 +0100 Subject: [PATCH 021/155] readme: update version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6bf94c25..df297cc9 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,8 @@ Optional: To install: - tar xvfj fail2ban-1.0.1.tar.bz2 - cd fail2ban-1.0.1 + tar xvfj fail2ban-1.0.2.tar.bz2 + cd fail2ban-1.0.2 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 From 17f060526e7d27c30c3d3caae73b3399c01d5840 Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Tue, 28 Feb 2023 11:36:34 +0100 Subject: [PATCH 022/155] readme: amend --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df297cc9..4d417586 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ / _|__ _(_) |_ ) |__ __ _ _ _ | _/ _` | | |/ /| '_ \/ _` | ' \ |_| \__,_|_|_/___|_.__/\__,_|_||_| - v1.0.1.dev1 20??/??/?? + v1.0.3.dev1 20??/??/?? ## Fail2Ban: ban hosts that cause multiple authentication errors @@ -47,8 +47,8 @@ Optional: To install: - tar xvfj fail2ban-1.0.2.tar.bz2 - cd fail2ban-1.0.2 + tar xvfj fail2ban-master.tar.bz2 + 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 From 234660e94d0b887aef8ae11d7826420de30a9bef Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Tue, 28 Feb 2023 11:39:00 +0100 Subject: [PATCH 023/155] CI-workflow: remove 3.5 (seems to have a bug in GHA now) --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 39c85231..e681e417 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: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11.0-beta.3', pypy2, pypy3] + python-version: [2.7, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11.0-beta.3', pypy2, pypy3] fail-fast: false # Steps represent a sequence of tasks that will be executed as part of the job steps: From a2c77429b9cd29e653a792463c6cf4fda6a0fbfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Kabele?= Date: Tue, 28 Feb 2023 07:34:16 +0100 Subject: [PATCH 024/155] New filter: routeros-auth.conf (Closes #3469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add filter to detect failed login attempts in the log produced by MikroTik RouterOS. - Add the filter to jail.conf - Add testcase for the filter Signed-off-by: Vít Kabele --- config/filter.d/routeros-auth.conf | 10 ++++++++++ config/jail.conf | 3 +++ fail2ban/tests/files/logs/routeros-auth | 15 +++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 config/filter.d/routeros-auth.conf create mode 100644 fail2ban/tests/files/logs/routeros-auth diff --git a/config/filter.d/routeros-auth.conf b/config/filter.d/routeros-auth.conf new file mode 100644 index 00000000..090296d4 --- /dev/null +++ b/config/filter.d/routeros-auth.conf @@ -0,0 +1,10 @@ +# Fail2Ban filter for failure attempts in MikroTik RouterOS +# +# + +[Definition] + +failregex = ^\s*\S+ system,error,critical login failure for user .*? from via \S+$ + +# Author: Vit Kabele + diff --git a/config/jail.conf b/config/jail.conf index f4990e09..741f62ee 100644 --- a/config/jail.conf +++ b/config/jail.conf @@ -958,6 +958,9 @@ port = http,https logpath = %(syslog_authpriv)s backend = %(syslog_backend)s +[routeros-auth] +port = ssh,http,https +logpath = /var/log/MikroTik/router.log [zoneminder] # Zoneminder HTTP/HTTPS web interface auth diff --git a/fail2ban/tests/files/logs/routeros-auth b/fail2ban/tests/files/logs/routeros-auth new file mode 100644 index 00000000..6b861c7b --- /dev/null +++ b/fail2ban/tests/files/logs/routeros-auth @@ -0,0 +1,15 @@ +# RouterOS v7.5 +# failJSON: { "time": "2005-02-15T11:25:46", "match": true , "host": "192.168.88.6", "user": "admin" } +Feb 15 11:25:46 gw.local system,error,critical login failure for user admin from 192.168.88.6 via web + +# RouterOS v7.5 +# failJSON: { "match": false } +Feb 15 11:26:15 gw.local system,info log rule changed by admin + +# RouterOS v7.5 +# failJSON: { "time": "2005-02-15T11:57:42", "match": true , "host": "2001:470:1:c84::24", "user": "" } +Feb 15 11:57:42 1234.hostname.cz system,error,critical login failure for user from 2001:470:1:c84::24 via ssh + +# RouterOS v7.5 +# failJSON: { "time": "2005-03-02T09:09:46", "match": true , "host": "1.2.3.4", "user": "user with space" } +Mar 2 09:09:46 gw.local system,error,critical login failure for user user with space from 1.2.3.4 via ssh From 9997807fb329b6c850e9c5ecb5564a234050763d Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Sun, 25 Oct 2020 13:46:26 +0000 Subject: [PATCH 025/155] Add action for mikrotik routerOS --- ChangeLog | 7 ++++ config/action.d/mikrotik.conf | 79 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 config/action.d/mikrotik.conf diff --git a/ChangeLog b/ChangeLog index a2259e36..bf8d6e76 100644 --- a/ChangeLog +++ b/ChangeLog @@ -293,7 +293,14 @@ ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools * fail2ban-client: extended to unban IP range(s) by subnet (CIDR/mask) or hostname (DNS), gh-2791; * extended capturing of alternate tags in filter, allowing combine of multiple groups to single tuple token with new tag prefix `` with all value of `` tags (gh-2755) +<<<<<<< HEAD +======= +* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair + of sane environment (in case of recognized unsane state) would only occur on action errors (e. g. + if ban or unban operations are exiting with other code as 0) +* new action for mikrotik routerOS, adds and removes entries from address lists on the router +>>>>>>> Add action for mikrotik routerOS ver. 0.11.1 (2020/01/11) - this-is-the-way ----------- diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf new file mode 100644 index 00000000..91e587aa --- /dev/null +++ b/config/action.d/mikrotik.conf @@ -0,0 +1,79 @@ +# Fail2Ban configuration file +# +# Mikrotik routerOS action to add/remove address-list entries +# +# Author: Duncan Bellamy +# based on forum.mikrotik.com post by pakjebakmeel +# +# in the instructions: +# (10.0.0.1 is ip of mikrotik router) +# (10.0.0.2 is ip of fail2ban machine) +# +# on fail2ban machine: +# sudo mkdir /var/lib/fail2ban/ssh +# sudo chmod 700 /var/lib/fail2ban/ssh +# sudo ssh-keygen -N "" -f /var/lib/fail2ban/ssh/fail2ban_id_rsa +# sudo scp /var/lib/fail2ban/ssh/fail2ban_id_rsa.pub admin@10.0.0.1:/ +# ssh admin@10.0.0.1 +# +# on mikrotik router: +# /user add name=miki-f2b group=write address=10.0.0.2 password="" +# /user ssh-keys import public-key-file=fail2ban_id_rsa.pub user=miki-f2b +# /quit +# +# on fail2ban machine: +# (check password login fails) +# ssh miki-f2b@10.0.0.1 +# (check private key works) +# sudo ssh -i /var/lib/fail2ban/ssh/fail2ban_id_rsa miki-f2b@10.0.0.1 +# +# Then create rules on mikrorik router that use address +# list(s) maintained by fail2ban eg in the forward chain +# drop from address list, or in the forward chain drop +# from address list to server +# +# example extract from jail.local overriding some defaults +# action = mikrotik[mtikkeyfile="%(mkeyfile)s", mtikuser="%(muser)s", mtikhost="%(mhost)s", mtiklistname="%(mlistname)s"] +# +# ignoreip = 127.0.0.1/8 192.168.0.0/24 + +# mkeyfile = /etc/fail2ban/ssh/mykey_id_rsa +# muser = myuser +# mhost = 192.168.0.1 +# mlistname = BAD LIST + +[Definition] + +actionstart = + +actionstop = + +actioncheck = + +actionban = %(mtikcommand)s "/ip firewall address-list add list=\"%(mtiklistname)s\" address= comment=%(mtikcomment)s" + +actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment=%(mtikcomment)s]" + +mtikcommand = ssh -l %(mtikuser)s -p%(mtikport)s -i %(mtikkeyfile)s %(mtikhost)s + +# Option: mktikuser +# Notes.: username to use when connecting to routerOS +mtikuser = +# Option: mtikport +# Notes.: port to use when connecting to routerOS +mtikport = 22 +# Option: mtikkeyfile +# Notes.: ssh private key to use for connecting to routerOS +mtikkeyfile = +# Option: mtikhost +# Notes.: hostname or ip of router +mtikhost = +# Option: mtiklistname +# Notes.: name of "address-list" to use on router +mtiklistname = Auto Fail2Ban +# Option: mtikcomment +# Notes.: comment to use on routerOS (must be unique as used for ip address removal) +mtikcomment = AutoF2B-- + +[Init] +name="%(__name__)s" From 0e3e9b1d7f67443c7dc046997718cc6924c83ef4 Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Wed, 25 Nov 2020 18:53:43 +0000 Subject: [PATCH 026/155] Add flushaction Change unban to find by ip address not comment --- config/action.d/mikrotik.conf | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index 91e587aa..cdf63bcb 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -4,7 +4,7 @@ # # Author: Duncan Bellamy # based on forum.mikrotik.com post by pakjebakmeel -# +## # in the instructions: # (10.0.0.1 is ip of mikrotik router) # (10.0.0.2 is ip of fail2ban machine) @@ -46,13 +46,15 @@ actionstart = -actionstop = +actionstop = %(actionflush)s + +actionflush = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment~\"%(startcomment)s*\"]" actioncheck = actionban = %(mtikcommand)s "/ip firewall address-list add list=\"%(mtiklistname)s\" address= comment=%(mtikcomment)s" -actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment=%(mtikcomment)s]" +actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" address=]" mtikcommand = ssh -l %(mtikuser)s -p%(mtikport)s -i %(mtikkeyfile)s %(mtikhost)s @@ -71,9 +73,12 @@ mtikhost = # Option: mtiklistname # Notes.: name of "address-list" to use on router mtiklistname = Auto Fail2Ban +# Option: startcomment +# Notes.: used as a prefix to all comments, and used to match for flushing rules +startcomment = AutoF2B # Option: mtikcomment # Notes.: comment to use on routerOS (must be unique as used for ip address removal) -mtikcomment = AutoF2B-- +mtikcomment = %(startcomment)s-- [Init] name="%(__name__)s" From ac2076ef4fac366fd62a10ec37964d51f23c803b Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Sun, 6 Dec 2020 10:53:21 +0000 Subject: [PATCH 027/155] change unban back to find comment so correct entry always deleted --- config/action.d/mikrotik.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index cdf63bcb..0f029af5 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -4,7 +4,7 @@ # # Author: Duncan Bellamy # based on forum.mikrotik.com post by pakjebakmeel -## +# # in the instructions: # (10.0.0.1 is ip of mikrotik router) # (10.0.0.2 is ip of fail2ban machine) @@ -54,7 +54,7 @@ actioncheck = actionban = %(mtikcommand)s "/ip firewall address-list add list=\"%(mtiklistname)s\" address= comment=%(mtikcomment)s" -actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" address=]" +actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment=%(mtikcomment)s]" mtikcommand = ssh -l %(mtikuser)s -p%(mtikport)s -i %(mtikkeyfile)s %(mtikhost)s From 5781675a7d8a8b9c48074d68ab9acd5964fc2368 Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Tue, 29 Dec 2020 13:17:41 +0000 Subject: [PATCH 028/155] change startcomment and comment so correct rules are flushed --- config/action.d/mikrotik.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index 0f029af5..300fde20 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -75,10 +75,10 @@ mtikhost = mtiklistname = Auto Fail2Ban # Option: startcomment # Notes.: used as a prefix to all comments, and used to match for flushing rules -startcomment = AutoF2B +startcomment = f2b- # Option: mtikcomment # Notes.: comment to use on routerOS (must be unique as used for ip address removal) -mtikcomment = %(startcomment)s-- +mtikcomment = %(startcomment)s- [Init] name="%(__name__)s" From d46ec3a5550daabcf040cac65ff71b5721d51333 Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Tue, 29 Dec 2020 15:30:32 +0100 Subject: [PATCH 029/155] add jail boundary to flush command for more precise targeting of jail (if some name may be equal to prefix of other name) --- config/action.d/mikrotik.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index 300fde20..96262587 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -48,7 +48,7 @@ actionstart = actionstop = %(actionflush)s -actionflush = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment~\"%(startcomment)s*\"]" +actionflush = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment~\"%(startcomment)s-*\"]" actioncheck = From b892133d516d1389a647a287a1a3b58e2eece65f Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Wed, 8 Mar 2023 09:20:51 +0000 Subject: [PATCH 030/155] move new comment in changelog --- ChangeLog | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/ChangeLog b/ChangeLog index bf8d6e76..bc92243c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -18,6 +18,7 @@ ver. 1.0.3-dev-1 (20??/??/??) - development nightly edition (value read from `/proc/sys/net/ipv6/conf/all/disable_ipv6`) if available, otherwise seeks over local IPv6 from network interfaces if available for platform and uses DNS to find local IPv6 as a fallback only * improve `ignoreself` by considering all local addresses from network interfaces additionally to IPs from hostnames (gh-3132) +* new action for mikrotik routerOS, adds and removes entries from address lists on the router ver. 1.0.2 (2022/11/09) - finally-war-game-test-tape-not-a-nuclear-alarm @@ -293,14 +294,7 @@ ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools * fail2ban-client: extended to unban IP range(s) by subnet (CIDR/mask) or hostname (DNS), gh-2791; * extended capturing of alternate tags in filter, allowing combine of multiple groups to single tuple token with new tag prefix `` with all value of `` tags (gh-2755) -<<<<<<< HEAD -======= -* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair - of sane environment (in case of recognized unsane state) would only occur on action errors (e. g. - if ban or unban operations are exiting with other code as 0) -* new action for mikrotik routerOS, adds and removes entries from address lists on the router ->>>>>>> Add action for mikrotik routerOS ver. 0.11.1 (2020/01/11) - this-is-the-way ----------- From 9b1417a16912562f15b5e1e252f16d6b1360fa41 Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Wed, 8 Mar 2023 09:29:03 +0000 Subject: [PATCH 031/155] apply suggestions --- config/action.d/mikrotik.conf | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index 96262587..30e9bac5 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -33,14 +33,14 @@ # from address list to server # # example extract from jail.local overriding some defaults -# action = mikrotik[mtikkeyfile="%(mkeyfile)s", mtikuser="%(muser)s", mtikhost="%(mhost)s", mtiklistname="%(mlistname)s"] +# action = mikrotik[keyfile="%(mkeyfile)s", user="%(muser)s", host="%(mhost)s", list="%(mlist)s"] # # ignoreip = 127.0.0.1/8 192.168.0.0/24 # mkeyfile = /etc/fail2ban/ssh/mykey_id_rsa # muser = myuser # mhost = 192.168.0.1 -# mlistname = BAD LIST +# mlist = BAD LIST [Definition] @@ -48,35 +48,35 @@ actionstart = actionstop = %(actionflush)s -actionflush = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment~\"%(startcomment)s-*\"]" +actionflush = %(command)s "/ip firewall address-list remove [find list=\"%(list)s\" comment~\"%(startcomment)s-*\"]" actioncheck = -actionban = %(mtikcommand)s "/ip firewall address-list add list=\"%(mtiklistname)s\" address= comment=%(mtikcomment)s" +actionban = %(command)s "/ip firewall address-list add list=\"%(list)s\" address= comment=%(comment)s" -actionunban = %(mtikcommand)s "/ip firewall address-list remove [find list=\"%(mtiklistname)s\" comment=%(mtikcomment)s]" +actionunban = %(command)s "/ip firewall address-list remove [find list=\"%(list)s\" comment=%(comment)s]" -mtikcommand = ssh -l %(mtikuser)s -p%(mtikport)s -i %(mtikkeyfile)s %(mtikhost)s +mtikcommand = ssh -l %(user)s -p%(port)s -i %(keyfile)s %(host)s -# Option: mktikuser +# Option: user # Notes.: username to use when connecting to routerOS mtikuser = -# Option: mtikport +# Option: port # Notes.: port to use when connecting to routerOS mtikport = 22 -# Option: mtikkeyfile +# Option: keyfile # Notes.: ssh private key to use for connecting to routerOS mtikkeyfile = -# Option: mtikhost +# Option: host # Notes.: hostname or ip of router mtikhost = -# Option: mtiklistname +# Option: list # Notes.: name of "address-list" to use on router -mtiklistname = Auto Fail2Ban +mtiklistname = Fail2Ban # Option: startcomment # Notes.: used as a prefix to all comments, and used to match for flushing rules startcomment = f2b- -# Option: mtikcomment +# Option: comment # Notes.: comment to use on routerOS (must be unique as used for ip address removal) mtikcomment = %(startcomment)s- From 7dc32971f8fa9fb4b4260e4a641aaedde68756d2 Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Wed, 8 Mar 2023 12:16:35 +0000 Subject: [PATCH 032/155] changed missed names --- config/action.d/mikrotik.conf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/action.d/mikrotik.conf b/config/action.d/mikrotik.conf index 30e9bac5..9343c86b 100644 --- a/config/action.d/mikrotik.conf +++ b/config/action.d/mikrotik.conf @@ -56,29 +56,29 @@ actionban = %(command)s "/ip firewall address-list add list=\"%(list)s\" address actionunban = %(command)s "/ip firewall address-list remove [find list=\"%(list)s\" comment=%(comment)s]" -mtikcommand = ssh -l %(user)s -p%(port)s -i %(keyfile)s %(host)s +command = ssh -l %(user)s -p%(port)s -i %(keyfile)s %(host)s # Option: user # Notes.: username to use when connecting to routerOS -mtikuser = +user = # Option: port # Notes.: port to use when connecting to routerOS -mtikport = 22 +port = 22 # Option: keyfile # Notes.: ssh private key to use for connecting to routerOS -mtikkeyfile = +keyfile = # Option: host # Notes.: hostname or ip of router -mtikhost = +host = # Option: list # Notes.: name of "address-list" to use on router -mtiklistname = Fail2Ban +list = Fail2Ban # Option: startcomment # Notes.: used as a prefix to all comments, and used to match for flushing rules startcomment = f2b- # Option: comment # Notes.: comment to use on routerOS (must be unique as used for ip address removal) -mtikcomment = %(startcomment)s- +comment = %(startcomment)s- [Init] name="%(__name__)s" From c7f8b75e7e013ac893daf78eacfdba98f2c9b689 Mon Sep 17 00:00:00 2001 From: "Sergey G. Brester" Date: Wed, 15 Mar 2023 15:03:48 +0100 Subject: [PATCH 033/155] action.d/cloudflare-token.conf: fixes #3479, url-encode args by unban --- config/action.d/cloudflare-token.conf | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/action.d/cloudflare-token.conf b/config/action.d/cloudflare-token.conf index 8c5c37de..287621eb 100644 --- a/config/action.d/cloudflare-token.conf +++ b/config/action.d/cloudflare-token.conf @@ -50,11 +50,12 @@ actionban = curl -s -X POST "<_cf_api_url>" \ #