New upstream version 0.11.1

This commit is contained in:
Sylvestre Ledru 2020-01-12 23:21:29 +01:00
parent dcb9340599
commit ad3e0d97c4
134 changed files with 5907 additions and 1373 deletions

View file

@ -1,21 +1,32 @@
# vim ft=yaml
# travis-ci.org definition for Fail2Ban build
# https://travis-ci.org/fail2ban/fail2ban/
#os: linux
language: python
python:
- 2.6
- 2.7
- pypy
# disabled until coverage module fixes up compatibility issue
# - 3.2
- 3.3
- 3.4
- 3.5
- 3.6
- 3.7-dev
# disabled since setuptools dropped support for Python 3.0 - 3.2
# - pypy3
- pypy3.3-5.5-alpha
dist: xenial
matrix:
fast_finish: true
include:
- python: 2.6
dist: trusty # required for Python 2.6
- python: 2.7
dist: trusty # required for packages like gamin
name: 2.7 (trusty)
- python: 2.7
name: 2.7 (xenial)
- python: pypy
dist: trusty
- python: 3.3
dist: trusty
- python: 3.4
- python: 3.5
- python: 3.6
- python: 3.7
- python: 3.8-dev
- python: pypy3.5
before_install:
- echo "running under $TRAVIS_PYTHON_VERSION"
- if [[ $TRAVIS_PYTHON_VERSION == 2* || $TRAVIS_PYTHON_VERSION == pypy* && $TRAVIS_PYTHON_VERSION != pypy3* ]]; then export F2B_PY=2; fi
@ -35,15 +46,22 @@ install:
# codecov:
- travis_retry pip install codecov
# dnspython or dnspython3
- if [[ "$F2B_PY" = 2 ]]; then travis_retry pip install dnspython; fi
- if [[ "$F2B_PY" = 3 ]]; then travis_retry pip install dnspython3; fi
- if [[ "$F2B_PY" = 2 ]]; then travis_retry pip install dnspython || echo 'not installed'; fi
- if [[ "$F2B_PY" = 3 ]]; then travis_retry pip install dnspython3 || echo 'not installed'; fi
# python systemd bindings:
- if [[ "$F2B_PY" = 2 ]]; then travis_retry sudo apt-get install -qq python-systemd || echo 'not installed'; fi
- if [[ "$F2B_PY" = 3 ]]; then travis_retry sudo apt-get install -qq python3-systemd || echo 'not installed'; fi
# gamin - install manually (not in PyPI) - travis-ci system Python is 2.7
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then travis_retry sudo apt-get install -qq python-gamin && cp /usr/share/pyshared/gamin.py /usr/lib/pyshared/python2.7/_gamin.so $VIRTUAL_ENV/lib/python2.7/site-packages/; fi
- if [[ $TRAVIS_PYTHON_VERSION == 2.7 ]]; then (travis_retry sudo apt-get install -qq python-gamin && cp /usr/share/pyshared/gamin.py /usr/lib/pyshared/python2.7/_gamin.so $VIRTUAL_ENV/lib/python2.7/site-packages/) || echo 'not installed'; fi
# pyinotify
- travis_retry pip install pyinotify
- travis_retry pip install pyinotify || echo 'not installed'
# Install helper tools
- sudo apt-get install shellcheck
before_script:
# Manually execute 2to3 for now
- if [[ "$F2B_PY" = 3 ]]; then ./fail2ban-2to3; fi
# (debug) output current preferred encoding:
- python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
script:
# Keep the legacy setup.py test approach of checking coverage for python2
- if [[ "$F2B_PY" = 2 ]]; then coverage run setup.py test; fi
@ -51,13 +69,14 @@ script:
- if [[ "$F2B_PY" = 3 ]]; then coverage run bin/fail2ban-testcases --verbosity=2; fi
# Use $VENV_BIN (not python) or else sudo will always run the system's python (2.7)
- sudo $VENV_BIN/pip install .
# Doc files should get installed on Travis under Linux
- test -e /usr/share/doc/fail2ban/FILTERS
# Doc files should get installed on Travis under Linux (python >= 3.8 seem to use another path segment)
- if [[ $TRAVIS_PYTHON_VERSION < 3.8 ]]; then test -e /usr/share/doc/fail2ban/FILTERS; fi
# Test initd script
- shellcheck -s bash -e SC1090,SC1091 files/debian-initd
after_success:
- if [[ "$F2B_COV" = 1 ]]; then coveralls; fi
- codecov
matrix:
fast_finish: true
# Might be worth looking into
#notifications:
# email: true

192
ChangeLog
View file

@ -6,53 +6,190 @@
Fail2Ban: Changelog
===================
Incompatibility list (compared to v.0.9):
ver. 0.11.1 (2020/01/11) - this-is-the-way
-----------
* Filter (or `failregex`) internal capture-groups:
### Compatibility:
* to v.0.10:
- 0.11 is totally compatible to 0.10 (configuration- and API-related stuff), but the database
got some new tables and fields (auto-converted during the first start), so once updated to 0.11, you
have to remove the database /var/lib/fail2ban/fail2ban.sqlite3 (or its different to 0.10 schema)
if you would need to downgrade to 0.10 for some reason.
* to v.0.9:
- Filter (or `failregex`) internal capture-groups:
- If you've your own `failregex` or custom filters using conditional match `(?P=host)`, you should
rewrite the regex like in example below resp. using `(?:(?P=ip4)|(?P=ip6)` instead of `(?P=host)`
(or `(?:(?P=ip4)|(?P=ip6)|(?P=dns))` corresponding your `usedns` and `raw` settings).
* If you've your own `failregex` or custom filters using conditional match `(?P=host)`, you should
rewrite the regex like in example below resp. using `(?:(?P=ip4)|(?P=ip6)` instead of `(?P=host)`
(or `(?:(?P=ip4)|(?P=ip6)|(?P=dns))` corresponding your `usedns` and `raw` settings).
Of course you can always define your own capture-group (like below `_cond_ip_`) to do this.
```
testln="1500000000 failure from 192.0.2.1: bad host 192.0.2.1"
fail2ban-regex "$testln" "^\s*failure from (?P<_cond_ip_><HOST>): bad host (?P=_cond_ip_)$"
```
- New internal groups (currently reserved for internal usage):
`ip4`, `ip6`, `dns`, `fid`, `fport`, additionally `user` and another captures in lower case if
mapping from tag `<F-*>` used in failregex (e. g. `user` by `<F-USER>`).
Of course you can always define your own capture-group (like below `_cond_ip_`) to do this.
```
testln="1500000000 failure from 192.0.2.1: bad host 192.0.2.1"
fail2ban-regex "$testln" "^\s*failure from (?P<_cond_ip_><HOST>): bad host (?P=_cond_ip_)$"
```
* New internal groups (currently reserved for internal usage):
`ip4`, `ip6`, `dns`, `fid`, `fport`, additionally `user` and another captures in lower case if
mapping from tag `<F-*>` used in failregex (e. g. `user` by `<F-USER>`).
* v.0.10 uses more precise date template handling, that can be theoretically incompatible to some
user configurations resp. `datepattern`.
- v.0.10 and 0.11 use more precise date template handling, that can be theoretically incompatible to some
user configurations resp. `datepattern`.
* Since v0.10 fail2ban supports the matching of IPv6 addresses, but not all ban actions are
IPv6-capable now.
ver. 0.10.5-dev-1 (20??/??/??) - development edition
-----------
- Since v0.10 fail2ban supports the matching of IPv6 addresses, but not all ban actions are
IPv6-capable now.
### Fixes
* purge database will be executed now (within observer).
* restoring currently banned ip after service restart fixed
(now < timeofban + bantime), ignore old log failures (already banned)
* upgrade database: update new created table `bips` with entries from table `bans` (allows restore
current bans after upgrade from version <= 0.10)
### New Features
* Increment ban time (+ observer) functionality introduced.
* Database functionality extended with bad ips.
* New tags (usable in actions):
- `<bancount>` - ban count of this offender if known as bad (started by 1 for unknown)
- `<bantime>` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.)
* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected);
Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`.
Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local).
### Enhancements
* algorithm of restore current bans after restart changed: update the restored ban-time (and therefore
end of ban) of the ticket with ban-time of jail (as maximum), for all tickets with ban-time greater
(or persistent); not affected if ban-time of the jail is unchanged between stop/start.
* added new setup-option `--without-tests` to skip building and installing of tests files (gh-2287).
* added new command `fail2ban-client get <JAIL> banip ?sep-char|--with-time?` to get the banned ip addresses (gh-1916).
ver. 0.10.5 (2020/01/10) - deserve-more-respect-a-jedis-weapon-must
-----------
Yes, Hrrrm...
### Fixes
* [compatibility] systemd backend: default flags changed to SYSTEM_ONLY(4), fixed in gh-2444 in order to ignore
user session files per default, so could prevent "Too many open files" errors on a lot of user sessions (see gh-2392)
* [grave] fixed parsing of multi-line filters (`maxlines` > 1) together with systemd backend,
now systemd-filter replaces newlines in message from systemd journal with `\n` (otherwise
multi-line parsing may be broken, because removal of matched string from multi-line buffer window
is confused by such extra new-lines, so they are retained and got matched on every followed
message, see gh-2431)
* [stability] prevent race condition - no unban if the bans occur continuously (gh-2410);
now an unban-check will happen not later than 10 tickets get banned regardless there are
still active bans available (precedence of ban over unban-check is 10 now)
* fixed read of included config-files (`.local` overwrites options of `.conf` for config-files
included with before/after)
* `action.d/abuseipdb.conf`: switched to use AbuseIPDB API v2 (gh-2302)
* `action.d/badips.py`: fixed start of banaction on demand (which may be IP-family related), gh-2390
* `action.d/helpers-common.conf`: rewritten grep arguments, now options `-wF` used to match only
whole words and fixed string (not as pattern), gh-2298
* `filter.d/apache-auth.conf`:
- ignore errors from mod_evasive in `normal` mode (mode-controlled now) (gh-2548);
- extended with option `mode` - `normal` (default) and `aggressive`
* `filter.d/sshd.conf`:
- matches `Bad protocol version identification` in `ddos` and `aggressive` modes (gh-2404).
- captures `Disconnecting ...: Change of username or service not allowed` (gh-2239, gh-2279)
- captures `Disconnected from ... [preauth]` (`extra`/`aggressive` mode and preauth phase only, gh-2239, gh-2279)
- captures `Disconnected from ... [preauth]`, preauth phase only, different handling by `extra`
(with supplied user only) and `ddos`/`aggressive` mode (gh-2115, gh-2239, gh-2279)
* `filter.d/mysqld-auth.conf`:
- MYSQL 8.0.13 compatibility (log-error-verbosity = 3), log-format contains few additional words
enclosed in brackets after "[Note]" (gh-2314)
* `filter.d/sendmail-reject.conf`:
- `mode=extra` now captures port IDs of `TLSMTA` and `MSA` (defaults for ports 465 and 587 on some distros)
* `files/fail2ban.service.in`: fixed systemd-unit template - missing nftables dependency (gh-2313)
* several `action.d/mail*`: fixed usage with multiple log files (ultimate fix for gh-976, gh-2341)
* `filter.d/sendmail-reject.conf`: fixed journal usage for some systems (e. g. CentOS): if only identifier
set to `sm-mta` (no unit `sendmail`) for some messages (gh-2385)
* `filter.d/asterisk.conf`: asterisk can log additional timestamp if logs into systemd-journal
(regex extended with optional part matching this, gh-2383)
* `filter.d/postfix.conf`:
- regexp's accept variable suffix code in status of postfix for precise messages (gh-2442)
- extended with new postfix filter mode `errors` to match "too many errors" (gh-2439),
also included within modes `normal`, `more` (`extra` and `aggressive`), since postfix
parameter `smtpd_hard_error_limit` is default 20 (additionally consider `maxretry`)
* `filter.d/named-refused.conf`:
- support BIND 9.11.0 log format (includes an additional field @0xXXX..., gh-2406);
- `prefregex` extended, more selective now (denied/NOTAUTH suffix moved from failregex, so no catch-all there anymore)
* `filter.d/sendmail-auth.conf`, `filter.d/sendmail-reject.conf` :
- ID in prefix can be longer as 14 characters (gh-2563);
* all filters would accept square brackets around IPv4 addresses also (e. g. monit-filter, gh-2494)
* avoids unhandled exception during flush (gh-2588)
* fixes pass2allow-ftp jail - due to inverted handling, action should prohibit access per default for any IP,
therefore reset start on demand parameter for this action (it will be started immediately by repair);
* auto-detection of IPv6 subsystem availability (important for not on-demand actions or jails, like pass2allow);
### New Features
* new replacement tags for failregex to match subnets in form of IP-addresses with CIDR mask (gh-2559):
- `<CIDR>` - helper regex to match CIDR (simple integer form of net-mask);
- `<SUBNET>` - regex to match sub-net adresses (in form of IP/CIDR, also single IP is matched, so part /CIDR is optional);
* grouped tags (`<ADDR>`, `<HOST>`, `<SUBNET>`) recognize IP addresses enclosed in square brackets
* new failregex-flag tag `<F-MLFGAINED>` for failregex, signaled that the access to service was gained
(ATM used similar to tag `<F-NOFAIL>`, but it does not add the log-line to matches, gh-2279)
* filters: introduced new configuration parameter `logtype` (default `file` for file-backends, and
`journal` for journal-backends, gh-2387); can be also set to `rfc5424` to force filters (which include common.conf)
to use RFC 5424 conform prefix-line per default (gh-2467);
* for better performance and safety the option `logtype` can be also used to
select short prefix-line for file-backends too for all filters using `__prefix_line` (`common.conf`),
if message logged only with `hostname svc[nnnn]` prefix (often the case on several systems):
```ini
[jail]
backend = auto
filter = flt[logtype=short]
```
* `filter.d/common.conf`: differentiate `__prefix_line` for file/journal logtype's (speedup and fix parsing
of systemd-journal);
* `filter.d/traefik-auth.conf`: used to ban hosts, that were failed through traefik
* `filter.d/znc-adminlog.conf`: new filter for ZNC (IRC bouncer); requires the adminlog module to be loaded
### Enhancements
* introduced new options: `dbmaxmatches` (fail2ban.conf) and `maxmatches` (jail.conf) to contol
how many matches per ticket fail2ban can hold in memory and store in database (gh-2402, gh-2118);
* fail2ban.conf: introduced new section `[Thread]` and option `stacksize` to configure default size
of the stack for threads running in fail2ban (gh-2356), it could be set in `fail2ban.local` to
avoid runtime error "can't start new thread" (see gh-969);
* jail-reader extended (amend to gh-1622): actions support multi-line options now (interpolations
containing new-line);
* fail2ban-client: extended to ban/unban multiple tickets (see gh-2351, gh-2349);
Syntax:
- `fail2ban-client set <jain> banip <ip1> ... <ipN>`
- `fail2ban-client set <jain> unbanip [--report-absent] <ip1> ... <ipN>`
* fail2ban-client: extended with new feature which allows to inform fail2ban about single or multiple
attempts (failure) for IP (resp. failure-ID), see gh-2351;
Syntax:
- `fail2ban-client set <jail> attempt <ip> [<failure-message1> ... <failure-messageN>]`
* `action.d/nftables.conf`:
- isolate fail2ban rules into a dedicated table and chain (gh-2254)
- `nftables-allports` supports multiple protocols in single rule now
- combined nftables actions to single action `nftables`:
* `nftables-common` is removed (replaced with single action `nftables` now)
* `nftables-allports` is obsolete, superseded by `nftables[type=allports]`
* `nftables-multiport` is obsolete, superseded by `nftables[type=multiport]`
- allowed multiple protocols in `nftables[type=multiport]` action (single set with multiple rules
in chain), following configuration in jail would replace 3 separate actions, see
https://github.com/fail2ban/fail2ban/pull/2254#issuecomment-534684675
* `action.d/badips.py`: option `loglevel` extended with level of summary message,
following example configuration logging summary with NOTICE and rest with DEBUG log-levels:
`action = badips.py[loglevel="debug, notice"]`
* samplestestcase.py (testSampleRegexsFactory) extended:
- allow coverage of journal logtype;
- new option `fileOptions` to set common filter/test options for whole test-file;
* large enhancement: auto-reban, improved invariant check and conditional operations (gh-2588):
- improves invariant check and repair (avoid unhandled exception, consider family on conditional operations, etc),
prepared for bulk re-ban in repair case (if bulk-ban becomes implemented);
- automatic reban (repeat banning action) after repair/restore sane environment, if already logged ticket causes
new failures (via new action operation `actionreban` or `actionban` if still not defined in action);
* introduces banning epoch for actions and tickets (to distinguish or recognize removed set of the tickets);
* invariant check avoids repair by unban/stop (unless parameter `actionrepair_on_unban` set to `true`);
* better handling for all conditional operations (distinguish families for certain operations like
repair/flush/stop, prepared for other families, e. g. if different handling for subnets expected, etc);
* partially implements gh-980 (more breakdown safe handling);
* closes gh-1680 (better as large-scale banning implementation with on-demand reban by failure,
at least unless a bulk-ban gets implemented);
* fail2ban-regex - several enhancements and fixes:
- improved usage output (don't put a long help if an error occurs);
- new option `--no-check-all` to avoid check of all regex's (first matched only);
- new option `-o`, `--out` to set token only provided in output (disables check-all and outputs only expected data).
ver. 0.10.4 (2018/10/04) - ten-four-on-due-date-ten-four
@ -303,9 +440,14 @@ TODO: implementing of options resp. other tasks from PR #1346
- `<fid>` - failure identifier (if raw resp. failures without IP address)
- `<ip-rev>` - PTR reversed representation of IP address
- `<ip-host>` - host name of the IP address
- `<bancount>` - ban count of this offender if known as bad (started by 1 for unknown)
- `<bantime>` - current ban-time of the ticket (prolongation can be retarded up to 10 sec.)
- `<F-...>` - interpolates to the corresponding filter group capture `...`
- `<fq-hostname>` - fully-qualified name of host (the same as `$(hostname -f)`)
- `<sh-hostname>` - short hostname (the same as `$(uname -n)`)
* Introduced new action command `actionprolong` to prolong ban-time (e. g. set new timeout if expected);
Several actions (like ipset, etc.) rewritten using net logic with `actionprolong`.
Note: because ban-time is dynamic, it was removed from jail.conf as timeout argument (check jail.local).
* Allow to use filter options by `fail2ban-regex`, example:
fail2ban-regex text.log "sshd[mode=aggressive]"
* Samples test case factory extended with filter options - dict in JSON to control
@ -379,6 +521,9 @@ ver. 0.10.0-alpha-1 (2016/07/14) - ipv6-support-etc
* testSocket: sporadical bug repaired - wait for server thread starts a socket (listener)
* testExecuteTimeoutWithNastyChildren: sporadical bug repaired - wait for pid file inside bash,
kill tree in any case (gh-1155)
* purge database will be executed now (within observer).
* restoring currently banned ip after service restart fixed
(now < timeofban + bantime), ignore old log failures (already banned)
* Fixed high-load of pyinotify-backend,
see https://github.com/fail2ban/fail2ban/issues/885#issuecomment-248964591
* Database: stability fix - repack cursor iterator as long as locked
@ -416,6 +561,9 @@ ver. 0.10.0-alpha-1 (2016/07/14) - ipv6-support-etc
- new conditional section functionality used in config resp. includes:
- [Init?family=inet4] - IPv4 qualified hosts only
- [Init?family=inet6] - IPv6 qualified hosts only
* Increment ban time (+ observer) functionality introduced.
Thanks Serg G. Brester (sebres)
* Database functionality extended with bad ips.
* New reload functionality (now totally without restart, unbanning/rebanning, etc.),
see gh-1557
* Several commands extended and new commands introduced:

View file

@ -42,7 +42,7 @@ config/action.d/mail-whois-lines.conf
config/action.d/mynetwatchman.conf
config/action.d/netscaler.conf
config/action.d/nftables-allports.conf
config/action.d/nftables-common.conf
config/action.d/nftables.conf
config/action.d/nftables-multiport.conf
config/action.d/nginx-block-map.conf
config/action.d/npf.conf
@ -81,7 +81,9 @@ config/filter.d/apache-pass.conf
config/filter.d/apache-shellshock.conf
config/filter.d/assp.conf
config/filter.d/asterisk.conf
config/filter.d/bitwarden.conf
config/filter.d/botsearch-common.conf
config/filter.d/centreon.conf
config/filter.d/common.conf
config/filter.d/counter-strike.conf
config/filter.d/courier-auth.conf
@ -145,11 +147,13 @@ config/filter.d/sshd.conf
config/filter.d/stunnel.conf
config/filter.d/suhosin.conf
config/filter.d/tine20.conf
config/filter.d/traefik-auth.conf
config/filter.d/uwimap-auth.conf
config/filter.d/vsftpd.conf
config/filter.d/webmin-auth.conf
config/filter.d/wuftpd.conf
config/filter.d/xinetd-fail.conf
config/filter.d/znc-adminlog.conf
config/filter.d/zoneminder.conf
config/jail.conf
config/paths-arch.conf
@ -203,6 +207,7 @@ fail2ban/server/jail.py
fail2ban/server/jails.py
fail2ban/server/jailthread.py
fail2ban/server/mytime.py
fail2ban/server/observer.py
fail2ban/server/server.py
fail2ban/server/strptime.py
fail2ban/server/ticket.py
@ -219,6 +224,7 @@ fail2ban/tests/actiontestcase.py
fail2ban/tests/banmanagertestcase.py
fail2ban/tests/clientbeautifiertestcase.py
fail2ban/tests/clientreadertestcase.py
fail2ban/tests/config/action.d/action.conf
fail2ban/tests/config/action.d/brokenaction.conf
fail2ban/tests/config/fail2ban.conf
fail2ban/tests/config/filter.d/simple.conf
@ -256,6 +262,7 @@ fail2ban/tests/files/config/apache-auth/digest_wrongrelm/.htpasswd
fail2ban/tests/files/config/apache-auth/noentry/.htaccess
fail2ban/tests/files/config/apache-auth/README
fail2ban/tests/files/database_v1.db
fail2ban/tests/files/database_v2.db
fail2ban/tests/files/filter.d/substition.conf
fail2ban/tests/files/filter.d/testcase01.conf
fail2ban/tests/files/filter.d/testcase-common.conf
@ -273,9 +280,11 @@ fail2ban/tests/files/logs/apache-pass
fail2ban/tests/files/logs/apache-shellshock
fail2ban/tests/files/logs/assp
fail2ban/tests/files/logs/asterisk
fail2ban/tests/files/logs/bitwarden
fail2ban/tests/files/logs/bsd/syslog-plain.txt
fail2ban/tests/files/logs/bsd/syslog-v.txt
fail2ban/tests/files/logs/bsd/syslog-vv.txt
fail2ban/tests/files/logs/centreon
fail2ban/tests/files/logs/counter-strike
fail2ban/tests/files/logs/courier-auth
fail2ban/tests/files/logs/courier-smtp
@ -332,14 +341,17 @@ fail2ban/tests/files/logs/solid-pop3d
fail2ban/tests/files/logs/squid
fail2ban/tests/files/logs/squirrelmail
fail2ban/tests/files/logs/sshd
fail2ban/tests/files/logs/sshd-journal
fail2ban/tests/files/logs/stunnel
fail2ban/tests/files/logs/suhosin
fail2ban/tests/files/logs/tine20
fail2ban/tests/files/logs/traefik-auth
fail2ban/tests/files/logs/uwimap-auth
fail2ban/tests/files/logs/vsftpd
fail2ban/tests/files/logs/webmin-auth
fail2ban/tests/files/logs/wuftpd
fail2ban/tests/files/logs/xinetd-fail
fail2ban/tests/files/logs/znc-adminlog
fail2ban/tests/files/logs/zoneminder
fail2ban/tests/files/logs/zzz-generic-example
fail2ban/tests/files/logs/zzz-sshd-obsolete-multiline
@ -356,6 +368,7 @@ fail2ban/tests/files/zzz-sshd-obsolete-multiline.log
fail2ban/tests/filtertestcase.py
fail2ban/tests/__init__.py
fail2ban/tests/misctestcase.py
fail2ban/tests/observertestcase.py
fail2ban/tests/samplestestcase.py
fail2ban/tests/servertestcase.py
fail2ban/tests/sockettestcase.py

View file

@ -2,7 +2,7 @@
/ _|__ _(_) |_ ) |__ __ _ _ _
| _/ _` | | |/ /| '_ \/ _` | ' \
|_| \__,_|_|_/___|_.__/\__,_|_||_|
v0.10.3.dev1 20??/??/??
v0.11.0.dev1 20??/??/??
## Fail2Ban: ban hosts that cause multiple authentication errors
@ -18,11 +18,12 @@ attempts, it cannot eliminate the risk presented by weak authentication.
Set up services to use only two factor, or public/private authentication
mechanisms if you really want to protect services.
<img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of the IPv6 addresses.
<img src="http://www.worldipv6launch.org/wp-content/themes/ipv6/downloads/World_IPv6_launch_logo.svg" height="52pt"/> | Since v0.10 fail2ban supports the matching of IPv6 addresses.
------|------
This README is a quick introduction to Fail2Ban. More documentation, FAQ, and HOWTOs
to be found on fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki)
to be found on fail2ban(1) manpage, [Wiki](https://github.com/fail2ban/fail2ban/wiki),
[Developers documentation](https://fail2ban.readthedocs.io/)
and the website: https://www.fail2ban.org
Installation:
@ -45,10 +46,16 @@ Optional:
To install:
tar xvfj fail2ban-0.10.3.tar.bz2
cd fail2ban-0.10.3
python setup.py install
tar xvfj fail2ban-0.11.0.tar.bz2
cd fail2ban-0.11.0
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, 0.11
git clone https://github.com/fail2ban/fail2ban.git
cd fail2ban
sudo python setup.py install
This will install Fail2Ban into the python library directory. The executable
scripts are placed into `/usr/bin`, and configuration in `/etc/fail2ban`.
@ -58,6 +65,9 @@ Fail2Ban should be correctly installed now. Just type:
to see if everything is alright. You should always use fail2ban-client and
never call fail2ban-server directly.
You can verify that you have the correct version installed with
fail2ban-client version
Please note that the system init/service script is not automatically installed.
To enable fail2ban as an automatic service, simply copy the script for your
@ -79,11 +89,11 @@ fail2ban(1) and jail.conf(5) manpages for further references.
Code status:
------------
* [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.png?branch=0.10)](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) travis-ci.org (0.10 branch) / [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.png?branch=master)](https://travis-ci.org/fail2ban/fail2ban) travis-ci.org (master branch)
* travis-ci.org: [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.11)](https://travis-ci.org/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.10)](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) (0.10 branch)
* [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.png?branch=0.10)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10)
* coveralls.io: [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.11)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.10)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10) / (0.10 branch)
* [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.10)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10)
* codecov.io: [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.11)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.11) (0.11 branch) / [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.10)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10) (0.10 branch)
Contact:
--------

2
THANKS
View file

@ -111,7 +111,7 @@ Russell Odom
SATO Kentaro
Sean DuBois
Sebastian Arcus
Serg G. Brester
Serg G. Brester (sebres)
Sergey Safarov
Shaun C.
Sireyessire

View file

@ -47,6 +47,9 @@
[Definition]
# bypass action for restored 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
@ -80,13 +83,10 @@ actioncheck =
# wherever you install the helper script. For the PHP helper script, see
# <https://wiki.shaunc.com/wikka.php?wakka=ReportingToAbuseIPDBWithFail2Ban>
#
# --ciphers ecdhe_ecdsa_aes_256_sha is used to workaround a
# "NSS error -12286" from curl as it attempts to connect using
# SSLv3. See https://www.centos.org/forums/viewtopic.php?t=52732
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = lgm=$(printf '%%s\n...' "<matches>"); curl --fail --tlsv1.1 --data "key=<abuseipdb_apikey>" --data-urlencode "comment=$lgm" --data "ip=<ip>" --data "category=<abuseipdb_category>" "https://www.abuseipdb.com/report/json"
actionban = lgm=$(printf '%%.1000s\n...' "<matches>"); curl -sSf "https://api.abuseipdb.com/api/v2/report" -H "Accept: application/json" -H "Key: <abuseipdb_apikey>" --data-urlencode "comment=$lgm" --data-urlencode "ip=<ip>" --data "categories=<abuseipdb_category>"
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
@ -101,5 +101,5 @@ actionunban =
# Notes Your API key from abuseipdb.com
# Values: STRING Default: None
# Register for abuseipdb [https://www.abuseipdb.com], get api key and set below.
# You will need to set the catagory in the action call.
# You will need to set the category in the action call.
abuseipdb_apikey =

View file

@ -31,8 +31,8 @@ else: # pragma: 3.x no cover
from urllib2 import Request, urlopen, HTTPError
from urllib import urlencode
from fail2ban.server.actions import ActionBase
from fail2ban.helpers import str2LogLevel
from fail2ban.server.actions import Actions, ActionBase, BanTicket
from fail2ban.helpers import splitwords, str2LogLevel
@ -54,9 +54,6 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
age : str, optional
Age of last report for bad IPs, per badips.com syntax.
Default "24h" (24 hours)
key : str, optional
Key issued by badips.com to report bans, for later retrieval
of personalised content.
banaction : str, optional
Name of banaction to use for blacklisting bad IPs. If `None`,
no blacklist of IPs will take place.
@ -67,14 +64,17 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
"postfix", but want to use whole "mail" category for blacklist.
Default `category`.
bankey : str, optional
Key issued by badips.com to blacklist IPs reported with the
associated key.
Key issued by badips.com to retrieve personal list
of blacklist IPs.
updateperiod : int, optional
Time in seconds between updating bad IPs blacklist.
Default 900 (15 minutes)
loglevel : int/str, optional
Log level of the message when an IP is (un)banned.
Default `DEBUG`.
Can be also supplied as two-value list (comma- or space separated) to
provide level of the summary message when a group of IPs is (un)banned.
Example `DEBUG,INFO`.
agent : str, optional
User agent transmitted to server.
Default `Fail2Ban/ver.`
@ -90,9 +90,9 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
def _Request(self, url, **argv):
return Request(url, headers={'User-Agent': self.agent}, **argv)
def __init__(self, jail, name, category, score=3, age="24h", key=None,
banaction=None, bancategory=None, bankey=None, updateperiod=900, loglevel='DEBUG', agent="Fail2Ban",
timeout=TIMEOUT):
def __init__(self, jail, name, category, score=3, age="24h",
banaction=None, bancategory=None, bankey=None, updateperiod=900,
loglevel='DEBUG', agent="Fail2Ban", timeout=TIMEOUT):
super(BadIPsAction, self).__init__(jail, name)
self.timeout = timeout
@ -100,11 +100,12 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
self.category = category
self.score = score
self.age = age
self.key = key
self.banaction = banaction
self.bancategory = bancategory or category
self.bankey = bankey
self.loglevel = str2LogLevel(loglevel)
loglevel = splitwords(loglevel)
self.sumloglevel = str2LogLevel(loglevel[-1])
self.loglevel = str2LogLevel(loglevel[0])
self.updateperiod = updateperiod
self._bannedips = set()
@ -281,13 +282,8 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
def _banIPs(self, ips):
for ip in ips:
try:
self._jail.actions[self.banaction].ban({
'ip': ip,
'failures': 0,
'matches': "",
'ipmatches': "",
'ipjailmatches': "",
})
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
self._jail.actions[self.banaction].ban(ai)
except Exception as e:
self._logSys.error(
"Error banning IP %s for jail '%s' with action '%s': %s",
@ -302,13 +298,8 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
def _unbanIPs(self, ips):
for ip in ips:
try:
self._jail.actions[self.banaction].unban({
'ip': ip,
'failures': 0,
'matches': "",
'ipmatches': "",
'ipjailmatches': "",
})
ai = Actions.ActionInfo(BanTicket(ip), self._jail)
self._jail.actions[self.banaction].unban(ai)
except Exception as e:
self._logSys.error(
"Error unbanning IP %s for jail '%s' with action '%s': %s",
@ -350,9 +341,13 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
s = ips - self._bannedips
p = len(s)
self._banIPs(s)
self._logSys.log(self.loglevel,
"Updated IPs for jail '%s' (-%d/+%d). Update again in %i seconds",
self._jail.name, m, p, self.updateperiod)
if m != 0 or p != 0:
self._logSys.log(self.sumloglevel,
"Updated IPs for jail '%s' (-%d/+%d)",
self._jail.name, m, p)
self._logSys.debug(
"Next update for jail '%' in %i seconds",
self._jail.name, self.updateperiod)
finally:
self._timer = threading.Timer(self.updateperiod, self.update)
self._timer.start()
@ -382,8 +377,6 @@ class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
"""
try:
url = "/".join([self._badips, "add", self.category, str(aInfo['ip'])])
if self.key:
url = "?".join([url, urlencode({'key': self.key})])
self._logSys.debug('badips.com: ban, url: %r', url)
response = urlopen(self._Request(url), timeout=self.timeout)
except HTTPError as response: # pragma: no cover

View file

@ -18,7 +18,7 @@ before = firewallcmd-common.conf
[Definition]
actionstart = ipset create <ipmset> hash:ip timeout <bantime><familyopt>
actionstart = ipset create <ipmset> hash:ip timeout <default-timeout><familyopt>
firewall-cmd --direct --add-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
actionflush = ipset flush <ipmset>
@ -29,6 +29,8 @@ actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 <acti
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
actionunban = ipset del <ipmset> <ip> -exist
[Init]
@ -40,11 +42,11 @@ actionunban = ipset del <ipmset> <ip> -exist
#
chain = INPUT_direct
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Option: default-timeout
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 600
bantime = 600
default-timeout = 600
# Option: actiontype
# Notes.: defines additions to the blocking rule

View file

@ -4,8 +4,9 @@
# _grep_logs_args = 'test'
# (printf %%b "Log-excerpt contains 'test':\n"; %(_grep_logs)s; printf %%b "Log-excerpt contains 'test':\n") | mail ...
#
_grep_logs = logpath="<logpath>"; grep <grepopts> -E %(_grep_logs_args)s $logpath | <greplimit>
_grep_logs_args = "(^|[^0-9a-fA-F:])$(echo '<ip>' | sed 's/\./\\./g')([^0-9a-fA-F:]|$)"
_grep_logs = logpath="<logpath>"; grep <grepopts> %(_grep_logs_args)s $logpath | <greplimit>
# options `-wF` used to match only whole words and fixed string (not as pattern)
_grep_logs_args = -wF "<ip>"
# Used for actions, that should not by executed if ticket was restored:
_bypass_if_restored = if [ '<restored>' = '1' ]; then exit 0; fi;

View file

@ -26,7 +26,7 @@ before = iptables-common.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 create <ipmset> hash:ip timeout <bantime><familyopt>
actionstart = ipset create <ipmset> hash:ip timeout <default-timeout><familyopt>
<iptables> -I <chain> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush
@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -m set --match-set <ipmset> src -j <blocktype
#
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
@ -61,11 +63,11 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Option: default-timeout
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 600
#
bantime = 600
default-timeout = 600
ipmset = f2b-<name>
familyopt =

View file

@ -26,7 +26,7 @@ before = iptables-common.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 create <ipmset> hash:ip timeout <bantime><familyopt>
actionstart = ipset create <ipmset> hash:ip timeout <default-timeout><familyopt>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush
@ -51,6 +51,8 @@ actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m
#
actionban = ipset add <ipmset> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
@ -61,11 +63,11 @@ actionunban = ipset del <ipmset> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Option: default-timeout
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 600
#
bantime = 600
default-timeout = 600
ipmset = f2b-<name>
familyopt =

View file

@ -17,7 +17,7 @@ _whois = whois <ip> || echo "missing whois program"
# character set before sending it to a mail program
# make sure you have 'file' and 'iconv' commands installed when opting for that
_whois_target_charset = UTF-8
_whois_convert_charset = whois <ip> |
_whois_convert_charset = (%(_whois)s) |
{ WHOIS_OUTPUT=$(cat) ; WHOIS_CHARSET=$(printf %%b "$WHOIS_OUTPUT" | file -b --mime-encoding -) ; printf %%b "$WHOIS_OUTPUT" | iconv -f $WHOIS_CHARSET -t %(_whois_target_charset)s//TRANSLIT - ; }
# choose between _whois and _whois_convert_charset in mail-whois-common.local

View file

@ -6,17 +6,12 @@
# Modified: Alexander Belykh <albel727@ngs.ru>
# adapted for nftables
#
# Obsolete: superseded by nftables[type=allports]
[INCLUDES]
before = nftables-common.conf
before = nftables.conf
[Definition]
# Option: nftables_mode
# Notes.: additional expressions for nftables filter rule
# Values: nftables expressions
#
nftables_mode = meta l4proto <protocol>
[Init]
type = allports

View file

@ -1,135 +0,0 @@
# Fail2Ban configuration file
#
# Author: Daniel Black
# Author: Cyril Jaquier
# Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf
# Modified: Alexander Belykh <albel727@ngs.ru>
# adapted for nftables
#
# This is a included configuration file and includes the definitions for the nftables
# used in all nftables based actions by default.
#
# The user can override the defaults in nftables-common.local
[INCLUDES]
after = nftables-common.local
[Definition]
# Option: nftables_mode
# Notes.: additional expressions for nftables filter rule
# Values: nftables expressions
#
nftables_mode = <protocol> dport \{ <port> \}
# 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
#
actionstart = <nftables> add set <nftables_family> <nftables_table> <set_name> \{ type <nftables_type>\; \}
<nftables> insert rule <nftables_family> <nftables_table> <chain> %(nftables_mode)s <address_family> saddr @<set_name> <blocktype>
_nft_list = <nftables> --handle --numeric list chain <nftables_family> <nftables_table> <chain>
_nft_get_handle_id = grep -m1 '<address_family> saddr @<set_name> <blocktype> # handle' | grep -oe ' handle [0-9]*'
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = HANDLE_ID=$(%(_nft_list)s | %(_nft_get_handle_id)s)
<nftables> delete rule <nftables_family> <nftables_table> <chain> $HANDLE_ID
<nftables> delete set <nftables_family> <nftables_table> <set_name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <nftables> list chain <nftables_family> <nftables_table> <chain> | grep -q '@<set_name>[ \t]'
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = <nftables> add element <nftables_family> <nftables_table> <set_name> \{ <ip> \}
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = <nftables> delete element <nftables_family> <nftables_table> <set_name> \{ <ip> \}
[Init]
# Option: nftables_type
# Notes.: address type to work with
# Values: [ipv4_addr | ipv6_addr] Default: ipv4_addr
#
nftables_type = ipv4_addr
# Option: nftables_family
# Notes.: address family to work in
# Values: [ip | ip6 | inet] Default: inet
#
nftables_family = inet
# Option: nftables_table
# Notes.: table in the address family to work in
# Values: STRING Default: filter
#
nftables_table = filter
# Option: chain
# Notes specifies the nftables chain to which the Fail2Ban rules should be
# added
# Values: STRING Default: input
chain = input
# Default name of the filtering set
#
name = default
# Option: port
# Notes.: specifies port to monitor
# Values: [ NUM | STRING ] Default:
#
port = ssh
# Option: protocol
# Notes.: internally used by config reader for interpolations.
# Values: [ tcp | udp ] Default: tcp
#
protocol = tcp
# Option: blocktype
# Note: This is what the action does with rules. This can be any jump target
# as per the nftables man page (section 8). Common values are drop
# reject, reject with icmp type host-unreachable
# Values: STRING
blocktype = reject
# Option: nftables
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
nftables = nft
# Option: set_name
# Notes.: The name of the nft set used to store banned addresses
# Values: STRING
set_name = f2b-<name>
# Option: address_family
# Notes.: The family of the banned addresses
# Values: [ ip | ip6 ]
address_family = ip
[Init?family=inet6]
nftables_type = ipv6_addr
set_name = f2b-<name>6
address_family = ip6

View file

@ -6,17 +6,12 @@
# Modified: Alexander Belykh <albel727@ngs.ru>
# adapted for nftables
#
# Obsolete: superseded by nftables[type=multiport]
[INCLUDES]
before = nftables-common.conf
before = nftables.conf
[Definition]
# Option: nftables_mode
# Notes.: additional expressions for nftables filter rule
# Values: nftables expressions
#
nftables_mode = <protocol> dport \{ <port> \}
[Init]
type = multiport

View file

@ -0,0 +1,203 @@
# Fail2Ban configuration file
#
# Author: Daniel Black
# Author: Cyril Jaquier
# Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf
# Modified: Alexander Belykh <albel727@ngs.ru>
# adapted for nftables
#
# This is a included configuration file and includes the definitions for the nftables
# used in all nftables based actions by default.
#
# The user can override the defaults in nftables-common.local
# Example: redirect flow to honeypot
#
# [Init]
# table_family = ip
# chain_type = nat
# chain_hook = prerouting
# chain_priority = -50
# blocktype = counter redirect to 2222
[INCLUDES]
after = nftables-common.local
[Definition]
# Option: type
# Notes.: type of the action.
# Values: [ multiport | allports ] Default: multiport
#
type = multiport
rule_match-custom =
rule_match-allports = meta l4proto \{ <protocol> \}
rule_match-multiport = $proto dport \{ <port> \}
match = <rule_match-<type>>
# Option: rule_stat
# Notes.: statement for nftables filter rule.
# leaving it empty will block all (include udp and icmp)
# Values: nftables statement
#
rule_stat = %(match)s <addr_family> saddr @<addr_set> <blocktype>
# optional interator over protocol's:
_nft_for_proto-custom-iter =
_nft_for_proto-custom-done =
_nft_for_proto-allports-iter =
_nft_for_proto-allports-done =
_nft_for_proto-multiport-iter = for proto in $(echo '<protocol>' | sed 's/,/ /g'); do
_nft_for_proto-multiport-done = done
_nft_list = <nftables> -a list chain <table_family> <table> <chain>
_nft_get_handle_id = grep -oP '@<addr_set>\s+.*\s+\Khandle\s+(\d+)$'
_nft_add_set = <nftables> add set <table_family> <table> <addr_set> \{ type <addr_type>\; \}
<_nft_for_proto-<type>-iter>
<nftables> add rule <table_family> <table> <chain> %(rule_stat)s
<_nft_for_proto-<type>-done>
_nft_del_set = { %(_nft_list)s | %(_nft_get_handle_id)s; } | while read -r hdl; do
<nftables> delete rule <table_family> <table> <chain> $hdl; done
<nftables> delete set <table_family> <table> <addr_set>
# Option: _nft_shutdown_table
# Notes.: command executed after the stop in order to delete table (it checks that no sets are available):
# Values: CMD
#
_nft_shutdown_table = { <nftables> list table <table_family> <table> | grep -qP '^\s+set\s+'; } || {
<nftables> delete table <table_family> <table>
}
# 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
#
actionstart = <nftables> add table <table_family> <table>
<nftables> -- add chain <table_family> <table> <chain> \{ type <chain_type> hook <chain_hook> priority <chain_priority> \; \}
%(_nft_add_set)s
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action);
# uses `nft flush set ...` and as fallback (e. g. unsupported) recreates the set (with references)
# Values: CMD
#
actionflush = { <nftables> flush set <table_family> <table> <addr_set> 2> /dev/null; } || {
%(_nft_del_set)s
%(_nft_add_set)s
}
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = %(_nft_del_set)s
<_nft_shutdown_table>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <nftables> list chain <table_family> <table> <chain> | grep -q '@<addr_set>[ \t]'
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionban = <nftables> add element <table_family> <table> <addr_set> \{ <ip> \}
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: See jail.conf(5) man page
# Values: CMD
#
actionunban = <nftables> delete element <table_family> <table> <addr_set> \{ <ip> \}
[Init]
# Option: table
# Notes.: main table to store chain and sets (automatically created on demand)
# Values: STRING Default: f2b-table
table = f2b-table
# Option: table_family
# Notes.: address family to work in
# Values: [ip | ip6 | inet] Default: inet
table_family = inet
# Option: chain
# Notes.: main chain to store rules
# Values: STRING Default: f2b-chain
chain = f2b-chain
# Option: chain_type
# Notes.: refers to the kind of chain to be created
# Values: [filter | route | nat] Default: filter
#
chain_type = filter
# Option: chain_hook
# Notes.: refers to the kind of chain to be created
# Values: [ prerouting | input | forward | output | postrouting ] Default: input
#
chain_hook = input
# Option: chain_priority
# Notes.: priority in the chain.
# Values: NUMBER Default: -1
#
chain_priority = -1
# Option: addr_type
# Notes.: address type to work with
# Values: [ipv4_addr | ipv6_addr] Default: ipv4_addr
#
addr_type = ipv4_addr
# Default name of the filtering set
#
name = default
# Option: port
# Notes.: specifies port to monitor
# Values: [ NUM | STRING ] Default:
#
port = ssh
# Option: protocol
# Notes.: internally used by config reader for interpolations.
# Values: [ tcp | udp ] Default: tcp
#
protocol = tcp
# Option: blocktype
# Note: This is what the action does with rules. This can be any jump target
# as per the nftables man page (section 8). Common values are drop,
# reject, reject with icmpx type host-unreachable, redirect to 2222
# Values: STRING
blocktype = reject
# Option: nftables
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
nftables = nft
# Option: addr_set
# Notes.: The name of the nft set used to store banned addresses
# Values: STRING
addr_set = addr-set-<name>
# Option: addr_family
# Notes.: The family of the banned addresses
# Values: [ ip | ip6 ]
addr_family = ip
[Init?family=inet6]
addr_family = ip6
addr_type = ipv6_addr
addr_set = addr6-set-<name>

View file

@ -105,4 +105,4 @@ actioncheck =
actionban = echo "\\\\<fid> 1;" >> '%(blck_lst_file)s'; %(blck_lst_reload)s
actionunban = id=$(echo "<fid>" | sed -e 's/[]\/$*.^|[]/\\&/g'); sed -i "/$id 1;/d" %(blck_lst_file)s; %(blck_lst_reload)s
actionunban = id=$(echo "<fid>" | sed -e 's/[]\/$*.^|[]/\\&/g'); sed -i "/^\\\\$id 1;$/d" %(blck_lst_file)s; %(blck_lst_reload)s

View file

@ -12,5 +12,5 @@ actioncheck =
actionban = /usr/libexec/afctl -a <ip> -t <bantime>
actionunban = /usr/libexec/afctl -r <ip>
[Init]
bantime = 2880
actionprolong = %(actionunban)s && %(actionban)s

View file

@ -24,7 +24,7 @@ actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on <fq-hostname>
The jail <name> has been started successfully.\n
Output will be buffered until <lines> lines are available.\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
@ -38,7 +38,7 @@ actionstop = if [ -f <tmpfile> ]; then
These hosts have been banned by Fail2Ban.\n
`cat <tmpfile>`
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
rm <tmpfile>
fi
printf %%b "Subject: [Fail2Ban] <name>: stopped on <fq-hostname>
@ -47,7 +47,7 @@ actionstop = if [ -f <tmpfile> ]; then
Hi,\n
The jail <name> has been stopped.\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
# Option: actioncheck
# Notes.: command executed once before each actionban command
@ -71,7 +71,7 @@ actionban = printf %%b "`date`: <ip> (<failures> failures)\n" >> <tmpfile>
These hosts have been banned by Fail2Ban.\n
`cat <tmpfile>`
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
rm <tmpfile>
fi

View file

@ -21,7 +21,7 @@ actionstart = printf %%b "Subject: [Fail2Ban] <name>: started on <fq-hostname>
Hi,\n
The jail <name> has been started successfully.\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
@ -34,7 +34,7 @@ actionstop = printf %%b "Subject: [Fail2Ban] <name>: stopped on <fq-hostname>
Hi,\n
The jail <name> has been stopped.\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
# Option: actioncheck
# Notes.: command executed once before each actionban command
@ -60,6 +60,10 @@ actionunban =
[Init]
# Your system mail command
#
mailcmd = /usr/sbin/sendmail -f "<sender>" "<dest>"
# Recipient mail address
#
dest = root

View file

@ -37,11 +37,11 @@ actionban = ( printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostn
Country:`geoiplookup -f /usr/share/GeoIP/GeoIP.dat "<ip>" | cut -d':' -f2-`
AS:`geoiplookup -f /usr/share/GeoIP/GeoIPASNum.dat "<ip>" | cut -d':' -f2-`
hostname: <ip-host>\n\n
Lines containing failures of <ip>\n";
Lines containing failures of <ip> (max <grepmax>)\n";
%(_grep_logs)s;
printf %%b "\n
Regards,\n
Fail2Ban" ) | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" ) | <mailcmd>
[Init]

View file

@ -7,6 +7,7 @@
[INCLUDES]
before = sendmail-common.conf
mail-whois-common.conf
[Definition]
@ -27,11 +28,11 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostnam
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`/usr/bin/whois <ip>`\n\n
`%(_whois_command)s`\n\n
Matches for <name> with <ipjailfailures> failures IP:<ip>\n
<ipjailmatches>\n\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
[Init]

View file

@ -7,6 +7,7 @@
[INCLUDES]
before = sendmail-common.conf
mail-whois-common.conf
[Definition]
@ -27,11 +28,11 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostnam
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`/usr/bin/whois <ip>`\n\n
`%(_whois_command)s`\n\n
Matches with <ipfailures> failures IP:<ip>\n
<ipmatches>\n\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
[Init]

View file

@ -7,6 +7,7 @@
[INCLUDES]
before = sendmail-common.conf
mail-whois-common.conf
helpers-common.conf
[Definition]
@ -27,13 +28,13 @@ actionban = ( printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostn
Hi,\n
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`/usr/bin/whois <ip> || echo missing whois program`\n\n
Lines containing failures of <ip>\n";
Here is more information about <ip> :\n"
%(_whois_command)s;
printf %%b "\nLines containing failures of <ip> (max <grepmax>)\n";
%(_grep_logs)s;
printf %%b "\n
Regards,\n
Fail2Ban" ) | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" ) | <mailcmd>
[Init]

View file

@ -7,6 +7,7 @@
[INCLUDES]
before = sendmail-common.conf
mail-whois-common.conf
[Definition]
@ -27,11 +28,11 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostnam
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`/usr/bin/whois <ip>`\n\n
`%(_whois_command)s`\n\n
Matches:\n
<matches>\n\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
[Init]

View file

@ -7,6 +7,7 @@
[INCLUDES]
before = sendmail-common.conf
mail-whois-common.conf
[Definition]
@ -27,9 +28,9 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostnam
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n\n
Here is more information about <ip> :\n
`/usr/bin/whois <ip> || echo missing whois program`\n
`%(_whois_command)s`\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
[Init]

View file

@ -27,7 +27,7 @@ actionban = printf %%b "Subject: [Fail2Ban] <name>: banned <ip> from <fq-hostnam
The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n
Regards,\n
Fail2Ban" | /usr/sbin/sendmail -f <sender> <dest>
Fail2Ban" | <mailcmd>
[Init]

View file

@ -51,7 +51,7 @@
# Values: CMD
#
actionstart = if ! ipset -quiet -name list f2b-<name> >/dev/null;
then ipset -quiet -exist create f2b-<name> hash:ip timeout <bantime>;
then ipset -quiet -exist create f2b-<name> hash:ip timeout <default-timeout>;
fi
# Option: actionstop
@ -68,6 +68,8 @@ actionstop = ipset flush f2b-<name>
#
actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
actionprolong = %(actionban)s
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
@ -76,10 +78,8 @@ actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
#
actionunban = ipset del f2b-<name> <ip> -exist
[Init]
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Option: default-timeout
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 600
#
bantime = 600
default-timeout = 600

View file

@ -159,25 +159,25 @@ class SMTPAction(ActionBase):
try:
self._logSys.debug("Connected to SMTP '%s', response: %i: %s",
self.host, *smtp.connect(self.host))
if self.user and self.password:
if self.user and self.password: # pragma: no cover (ATM no tests covering that)
smtp.login(self.user, self.password)
failed_recipients = smtp.sendmail(
self.fromaddr, self.toaddr.split(", "), msg.as_string())
except smtplib.SMTPConnectError:
except smtplib.SMTPConnectError: # pragma: no cover
self._logSys.error("Error connecting to host '%s'", self.host)
raise
except smtplib.SMTPAuthenticationError:
except smtplib.SMTPAuthenticationError: # pragma: no cover
self._logSys.error(
"Failed to authenticate with host '%s' user '%s'",
self.host, self.user)
raise
except smtplib.SMTPException:
except smtplib.SMTPException: # pragma: no cover
self._logSys.error(
"Error sending mail to host '%s' from '%s' to '%s'",
self.host, self.fromaddr, self.toaddr)
raise
else:
if failed_recipients:
if failed_recipients: # pragma: no cover
self._logSys.warning(
"Email to '%s' failed to following recipients: %r",
self.toaddr, failed_recipients)
@ -186,7 +186,7 @@ class SMTPAction(ActionBase):
try:
self._logSys.debug("Disconnected from '%s', response %i: %s",
self.host, *smtp.quit())
except smtplib.SMTPServerDisconnected:
except smtplib.SMTPServerDisconnected: # pragma: no cover
pass # Not connected
def start(self):

View file

@ -41,7 +41,12 @@ actionstop =
actioncheck =
actionban = oifs=${IFS}; IFS=.;SEP_IP=( <ip> ); set -- ${SEP_IP}; ADDRESSES=$(dig +short -t txt -q $4.$3.$2.$1.abuse-contacts.abusix.org); IFS=${oifs}
actionban = oifs=${IFS};
RESOLVER_ADDR="%(addr_resolver)s"
if [ "<debug>" -gt 0 ]; then echo "try to resolve $RESOLVER_ADDR"; fi
ADDRESSES=$(dig +short -t txt -q $RESOLVER_ADDR | tr -d '"')
IFS=,; ADDRESSES=$(echo $ADDRESSES)
IFS=${oifs}
IP=<ip>
FROM=<sender>
SERVICE=<service>
@ -51,26 +56,37 @@ actionban = oifs=${IFS}; IFS=.;SEP_IP=( <ip> ); set -- ${SEP_IP}; ADDRESSES=$(di
PORT=<port>
DATE=`LC_ALL=C date --date=@<time> +"%%a, %%d %%h %%Y %%T %%z"`
if [ ! -z "$ADDRESSES" ]; then
oifs=${IFS}; IFS=,; ADDRESSES=$(echo $ADDRESSES)
IFS=${oifs}
(printf -- %%b "<header>\n<message>\n<report>\n\n";
date '+Note: Local timezone is %%z (%%Z)';
printf -- %%b "\n<ipmatches>\n\n<footer>") | <mailcmd> <mailargs> ${ADDRESSES//,/\" \"}
printf -- %%b "\n<ipmatches>\n\n<footer>") | <mailcmd> <mailargs> $ADDRESSES
fi
actionunban =
[Init]
# Server as resolver used in dig command
#
addr_resolver = <ip-rev>abuse-contacts.abusix.org
# Option: boundary
# Notes: This can be overwritten to be safe for possible predictions
boundary = bfbb0f920793ac03cb8634bde14d8a1e
_boundary = Abuse<time>-<boundary>
# Option: header
# Notes: This is really a fixed value
header = Subject: abuse report about $IP - $DATE\nAuto-Submitted: auto-generated\nX-XARF: PLAIN\nContent-Transfer-Encoding: 7bit\nContent-Type: multipart/mixed; charset=utf8;\n boundary=Abuse-bfbb0f920793ac03cb8634bde14d8a1e;\n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8;\n
header = Subject: abuse report about $IP - $DATE\nAuto-Submitted: auto-generated\nX-XARF: PLAIN\nContent-Transfer-Encoding: 7bit\nContent-Type: multipart/mixed; charset=utf8;\n boundary=%(_boundary)s;\n\n--%(_boundary)s\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8;\n
# Option: footer
# Notes: This is really a fixed value and needs to match the report and header
# mime delimiters
footer = \n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e--
footer = \n\n--%(_boundary)s--
# Option: report
# Notes: Intended to be fixed
report = --Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8; name=\"report.txt\";\n\n---\nReported-From: $FROM\nCategory: abuse\nReport-ID: $REPORTID\nReport-Type: login-attack\nService: $SERVICE\nVersion: 0.2\nUser-Agent: Fail2ban v0.9\nDate: $DATE\nSource-Type: ip-address\nSource: $IP\nPort: $PORT\nSchema-URL: http://www.x-arf.org/schema/abuse_login-attack_0.1.2.json\nAttachment: text/plain\nOccurances: $FAILURES\nTLP: $TLP\n\n\n--Abuse-bfbb0f920793ac03cb8634bde14d8a1e\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf8; name=\"logfile.log\";
report = --%(_boundary)s\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf-8; name=\"report.txt\";\n\n---\nReported-From: $FROM\nCategory: abuse\nReport-ID: $REPORTID\nReport-Type: login-attack\nService: $SERVICE\nVersion: 0.2\nUser-Agent: Fail2ban v0.9\nDate: $DATE\nSource-Type: ip-address\nSource: $IP\nPort: $PORT\nSchema-URL: http://www.x-arf.org/schema/abuse_login-attack_0.1.2.json\nAttachment: text/plain\nOccurances: $FAILURES\nTLP: $TLP\n\n\n--%(_boundary)s\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain; charset=utf8; name=\"logfile.log\";
# Option: Message
# Notes: This can be modified by the users

View file

@ -5,11 +5,11 @@
# Changes: in most of the cases you should not modify this
# file, but provide customizations in fail2ban.local file, e.g.:
#
# [Definition]
# [DEFAULT]
# loglevel = DEBUG
#
[Definition]
[DEFAULT]
# Option: loglevel
# Notes.: Set the log level output.
@ -67,3 +67,20 @@ dbfile = /var/lib/fail2ban/fail2ban.sqlite3
# Notes.: Sets age at which bans should be purged from the database
# Values: [ SECONDS ] Default: 86400 (24hours)
dbpurgeage = 1d
# Options: dbmaxmatches
# Notes.: Number of matches stored in database per ticket (resolvable via
# tags <ipmatches>/<ipjailmatches> in actions)
# Values: [ INT ] Default: 10
dbmaxmatches = 10
[Definition]
[Thread]
# Options: stacksize
# Notes.: Specifies the stack size (in KiB) to be used for subsequently created threads,
# and must be 0 or a positive integer value of at least 32.
# Values: [ SIZE ] Default: 0 (use platform or configured default)
#stacksize = 0

View file

@ -9,6 +9,16 @@ before = apache-common.conf
[Definition]
# Mode for filter: normal (default) and aggressive (allows DDoS & brute force detection of mod_evasive)
mode = normal
# ignore messages of mod_evasive module:
apache-pref-ign-normal = (?!evasive)
# allow "denied by server configuration" from all modules:
apache-pref-ign-aggressive =
# mode related ignore prefix for common _apache_error_client substitution:
apache-pref-ignore = <apache-pref-ign-<mode>>
prefregex = ^%(_apache_error_client)s (?:AH\d+: )?<F-CONTENT>.+</F-CONTENT>$
# auth_type = ((?:Digest|Basic): )?

View file

@ -27,7 +27,9 @@ _daemon = (?:apache\d*|httpd(?:/\w+)?)
apache-prefix = <apache-prefix-<logging>>
_apache_error_client = <apache-prefix>\[(:?error|\S+:\S+)\]( \[pid \d+(:\S+ \d+)?\])? \[client <HOST>(:\d{1,5})?\]
apache-pref-ignore =
_apache_error_client = <apache-prefix>\[(:?error|<apache-pref-ignore>\S+:\S+)\]( \[pid \d+(:\S+ \d+)?\])? \[client <HOST>(:\d{1,5})?\]
datepattern = {^LN-BEG}

View file

@ -10,7 +10,7 @@ before = apache-common.conf
[Definition]
failregex = ^%(_apache_error_client)s ModSecurity:\s+(?:\[(?:\w+ \"[^\"]*\"|[^\]]*)\]\s*)*Access denied with code [45]\d\d
failregex = ^%(_apache_error_client)s(?: \[client [^\]]+\])? ModSecurity:\s+(?:\[(?:\w+ \"[^\"]*\"|[^\]]*)\]\s*)*Access denied with code [45]\d\d
ignoreregex =

View file

@ -23,7 +23,7 @@ prefregex = ^%(_apache_error_client)s (?:AH0(?:01(?:28|30)|1(?:264|071)): )?(?:(
failregex = ^(?:does not exist|not found or unable to stat): <script>\b
^'<script>\S*' not found or unable to stat
^error '[Pp]rimary script unknown\\n'
^error '[Pp]rimary script unknown(?:\\n)?'
ignoreregex =

View file

@ -18,14 +18,16 @@ iso8601 = \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{4}
# All Asterisk log messages begin like this:
log_prefix= (?:NOTICE|SECURITY|WARNING)%(__pid_re)s:?(?:\[C-[\da-f]*\])?:? [^:]+:\d*(?:(?: in)? [^:]+:)?
failregex = ^%(__prefix_line)s%(log_prefix)s Registration from '[^']*' failed for '<HOST>(:\d+)?' - (?:Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)$
^%(__prefix_line)s%(log_prefix)s Call from '[^']*' \(<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
^%(__prefix_line)s%(log_prefix)s (?:Host )?<HOST> (?:failed (?:to authenticate\b|MD5 authentication\b)|tried to authenticate with nonexistent user\b)
^%(__prefix_line)s%(log_prefix)s No registration for peer '[^']*' \(from <HOST>\)$
^%(__prefix_line)s%(log_prefix)s hacking attempt detected '<HOST>'$
^%(__prefix_line)s%(log_prefix)s SecurityEvent="(?:FailedACL|InvalidAccountID|ChallengeResponseFailed|InvalidPassword)"(?:(?:,(?!RemoteAddress=)\w+="[^"]*")*|.*?),RemoteAddress="IPV[46]/(UDP|TCP|WS)/<HOST>/\d+"(?:,(?!RemoteAddress=)\w+="[^"]*")*$
^%(__prefix_line)s%(log_prefix)s "Rejecting unknown SIP connection from <HOST>(?::\d+)?"$
^%(__prefix_line)s%(log_prefix)s Request (?:'[^']*' )?from '(?:[^']*|.*?)' failed for '<HOST>(?::\d+)?'\s\(callid: [^\)]*\) - (?:No matching endpoint found|Not match Endpoint(?: Contact)? ACL|(?:Failed|Error) to authenticate)\s*$
prefregex = ^%(__prefix_line)s%(log_prefix)s <F-CONTENT>.+</F-CONTENT>$
failregex = ^Registration from '[^']*' failed for '<HOST>(:\d+)?' - (?:Wrong password|Username/auth name mismatch|No matching peer found|Not a local domain|Device does not match ACL|Peer is not supposed to register|ACL error \(permit/deny\)|Not a local domain)$
^Call from '[^']*' \(<HOST>:\d+\) to extension '[^']*' rejected because extension not found in context
^(?:Host )?<HOST> (?:failed (?:to authenticate\b|MD5 authentication\b)|tried to authenticate with nonexistent user\b)
^No registration for peer '[^']*' \(from <HOST>\)$
^hacking attempt detected '<HOST>'$
^SecurityEvent="(?:FailedACL|InvalidAccountID|ChallengeResponseFailed|InvalidPassword)"(?:(?:,(?!RemoteAddress=)\w+="[^"]*")*|.*?),RemoteAddress="IPV[46]/[^/"]+/<HOST>/\d+"(?:,(?!RemoteAddress=)\w+="[^"]*")*$
^"Rejecting unknown SIP connection from <HOST>(?::\d+)?"$
^Request (?:'[^']*' )?from '(?:[^']*|.*?)' failed for '<HOST>(?::\d+)?'\s\(callid: [^\)]*\) - (?:No matching endpoint found|Not match Endpoint(?: Contact)? ACL|(?:Failed|Error) to authenticate)\s*$
# FreePBX (todo: make optional in v.0.10):
# ^(%(__prefix_line)s|\[\]\s*WARNING%(__pid_re)s:?(?:\[C-[\da-f]*\])? )[^:]+: Friendly Scanner from <HOST>$
@ -42,3 +44,12 @@ datepattern = {^LN-BEG}
# First regex: channels/chan_sip.c
#
# main/logger.c:ast_log_vsyslog - "in {functionname}:" only occurs in syslog
journalmatch = _SYSTEMD_UNIT=asterisk.service
[lt_journal]
# asterisk can log timestamp if logs into systemd-journal (optional part matching this timestamp, gh-2383):
__extra_timestamp = (?:\[[^\]]+\]\s+)?
__prefix_line = %(known/__prefix_line)s%(__extra_timestamp)s

View file

@ -0,0 +1,6 @@
# Fail2Ban filter for Bitwarden
# Detecting failed login attempts
# Logged in bwdata/logs/identity/Identity/log.txt
[Definition]
failregex = ^\s*\[WRN\]\s+Failed login attempt(?:, 2FA invalid)?\. <HOST>$

View file

@ -0,0 +1,9 @@
# Fail2Ban filter for Centreon Web
# Detecting unauthorized access to the Centreon Web portal
# typically logged in /var/log/centreon/login.log
[Init]
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%S
[Definition]
failregex = ^(?:\|-?\d+){3}\|\[[^\]]*\] \[<HOST>\] Authentication failed for '<F-USER>[^']+</F-USER>'

View file

@ -10,6 +10,9 @@ after = common.local
[DEFAULT]
# Type of log-file resp. log-format (file, short, journal, rfc542):
logtype = file
# Daemon definition is to be specialized (if needed) in .conf file
_daemon = \S*
@ -34,7 +37,7 @@ __daemon_combs_re = (?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_r
# Some messages have a kernel prefix with a timestamp
# EXAMPLES: kernel: [769570.846956]
__kernel_prefix = kernel: \[ *\d+\.\d+\]
__kernel_prefix = kernel:\s?\[ *\d+\.\d+\]:?
__hostname = \S+
@ -55,13 +58,32 @@ __date_ambit = (?:\[\])
# [bsdverbose]? [hostname] [vserver tag] daemon_id spaces
#
# This can be optional (for instance if we match named native log files)
__prefix_line = %(__date_ambit)s?\s*(?:%(__bsd_syslog_verbose)s\s+)?(?:%(__hostname)s\s+)?(?:%(__kernel_prefix)s\s+)?(?:%(__vserver)s\s+)?(?:%(__daemon_combs_re)s\s+)?(?:%(__daemon_extra_re)s\s+)?
__prefix_line = <lt_<logtype>/__prefix_line>
# PAM authentication mechanism check for failures, e.g.: pam_unix, pam_sss,
# pam_ldap
__pam_auth = pam_unix
# standardly all formats using prefix have line-begin anchored date:
datepattern = <lt_<logtype>/datepattern>
[lt_file]
# Common line prefixes for logtype "file":
__prefix_line = %(__date_ambit)s?\s*(?:%(__bsd_syslog_verbose)s\s+)?(?:%(__hostname)s\s+)?(?:%(__kernel_prefix)s\s+)?(?:%(__vserver)s\s+)?(?:%(__daemon_combs_re)s\s+)?(?:%(__daemon_extra_re)s\s+)?
datepattern = {^LN-BEG}
# Author: Yaroslav Halchenko
[lt_short]
# Common (short) line prefix for logtype "journal" (corresponds output of formatJournalEntry):
__prefix_line = \s*(?:%(__hostname)s\s+)?(?:%(_daemon)s%(__pid_re)s?:?\s+)?(?:%(__kernel_prefix)s\s+)?
datepattern = %(lt_file/datepattern)s
[lt_journal]
__prefix_line = %(lt_short/__prefix_line)s
datepattern = %(lt_short/datepattern)s
[lt_rfc5424]
# RFC 5424 log-format, see gh-2309:
#__prefix_line = \s*<__hostname> <__daemon_re> \d+ \S+ \S+\s+
__prefix_line = \s*<__hostname> <__daemon_re> \d+ \S+ (?:[^\[\]\s]+|(?:\[(?:[^\]"]*|"[^"]*")*\])+)\s+
datepattern = ^<\d+>\d+\s+{DATE}
# Author: Yaroslav Halchenko, Sergey G. Brester (aka sebres)

View file

@ -24,7 +24,7 @@ def is_googlebot(ip):
import re
host = DNSUtils.ipToName(ip)
if not host or not re.match('.*\.google(bot)?\.com$', host):
if not host or not re.match(r'.*\.google(bot)?\.com$', host):
return False
host_ips = DNSUtils.dnsToIp(host)
return (ip in host_ips)

View file

@ -3,7 +3,7 @@
#
# To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld]:
# log-error=/var/log/mysqld.log
# log-warning = 2
# log-warnings = 2
#
# If using mysql syslog [mysql_safe] has syslog in /etc/my.cnf
@ -17,7 +17,7 @@ before = common.conf
_daemon = mysqld
failregex = ^%(__prefix_line)s(?:\d+ |\d{6} \s?\d{1,2}:\d{2}:\d{2} )?\[\w+\] (?:\[[^\]]+\] )*Access denied for user '[^']+'@'<HOST>' (to database '[^']*'|\(using password: (YES|NO)\))*\s*$
failregex = ^%(__prefix_line)s(?:(?:\d{6}|\d{4}-\d{2}-\d{2})[ T]\s?\d{1,2}:\d{2}:\d{2} )?(?:\d+ )?\[\w+\] (?:\[[^\]]+\] )*Access denied for user '[^']+'@'<HOST>' (to database '[^']*'|\(using password: (YES|NO)\))*\s*$
ignoreregex =

View file

@ -34,11 +34,11 @@ __daemon_combs_re=(?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_re)
# this can be optional (for instance if we match named native log files)
__line_prefix=(?:\s\S+ %(__daemon_combs_re)s\s+)?
prefregex = ^%(__line_prefix)s( error:)?\s*client <HOST>#\S+( \([\S.]+\))?: <F-CONTENT>.+</F-CONTENT>$
prefregex = ^%(__line_prefix)s(?: error:)?\s*client(?: @\S*)? <HOST>#\S+(?: \([\S.]+\))?: <F-CONTENT>.+</F-CONTENT>\s(?:denied|\(NOTAUTH\))\s*$
failregex = ^(view (internal|external): )?query(?: \(cache\))? '.*' denied\s*$
^zone transfer '\S+/AXFR/\w+' denied\s*$
^bad zone transfer request: '\S+/IN': non-authoritative zone \(NOTAUTH\)\s*$
failregex = ^(?:view (?:internal|external): )?query(?: \(cache\))?
^zone transfer
^bad zone transfer request: '\S+/IN': non-authoritative zone
ignoreregex =

View file

@ -15,13 +15,13 @@ _port = (?::\d+)?
prefregex = ^%(__prefix_line)s<mdpr-<mode>> <F-CONTENT>.+</F-CONTENT>$
mdpr-normal = (?:NOQUEUE: reject:|improper command pipelining after \S+)
mdpr-normal = (?:\w+: reject:|(?:improper command pipelining|too many errors) after \S+)
mdre-normal=^RCPT from [^[]*\[<HOST>\]%(_port)s: 55[04] 5\.7\.1\s
^RCPT from [^[]*\[<HOST>\]%(_port)s: 45[04] 4\.7\.1 (?:Service unavailable\b|Client host rejected: cannot find your (reverse )?hostname\b)
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.7\.1 (<[^>]*>)?: Helo command rejected: Host not found\b
^EHLO from [^[]*\[<HOST>\]%(_port)s: 504 5\.5\.2 (<[^>]*>)?: Helo command rejected: need fully-qualified hostname\b
^VRFY from [^[]*\[<HOST>\]%(_port)s: 550 5\.1\.1\s
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.1\.8 (<[^>]*>)?: Sender address rejected: Domain not found\b
^RCPT from [^[]*\[<HOST>\]%(_port)s: 45[04] 4\.7\.\d+ (?:Service unavailable\b|Client host rejected: cannot find your (reverse )?hostname\b)
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.7\.\d+ (<[^>]*>)?: Helo command rejected: Host not found\b
^EHLO from [^[]*\[<HOST>\]%(_port)s: 504 5\.5\.\d+ (<[^>]*>)?: Helo command rejected: need fully-qualified hostname\b
^(RCPT|VRFY) from [^[]*\[<HOST>\]%(_port)s: 550 5\.1\.1\s
^RCPT from [^[]*\[<HOST>\]%(_port)s: 450 4\.1\.\d+ (<[^>]*>)?: Sender address rejected: Domain not found\b
^from [^[]*\[<HOST>\]%(_port)s:?
mdpr-auth = warning:
@ -48,6 +48,8 @@ mdpr-aggressive = (?:%(mdpr-auth)s|%(mdpr-normal)s|%(mdpr-ddos)s)
mdre-aggressive = %(mdre-auth2)s
%(mdre-normal)s
mdpr-errors = too many errors after \S+
mdre-errors = ^from [^[]*\[<HOST>\]%(_port)s$
failregex = <mdre-<mode>>
@ -56,10 +58,17 @@ failregex = <mdre-<mode>>
# Usage example (for jail.local):
# [postfix]
# mode = aggressive
#
# # or another jail (rewrite filter parameters of jail):
# [postfix-rbl]
# filter = postfix[mode=rbl]
#
# # jail to match "too many errors", related postconf `smtpd_hard_error_limit`:
# # (normally included in other modes (normal, more, extra, aggressive), but this jail'd allow to ban on the first message)
# [postfix-many-errors]
# filter = postfix[mode=errors]
# maxretry = 1
#
mode = more
ignoreregex =

View file

@ -8,8 +8,10 @@ before = common.conf
[Definition]
_daemon = (?:sendmail|sm-(?:mta|acceptingconnections))
__prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )?
failregex = ^%(__prefix_line)s\w{14}: (\S+ )?\[(?:IPv6:<IP6>|<IP4>)\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$
# "w{14,20}" will give support for IDs from 14 up to 20 characters long
failregex = ^%(__prefix_line)s(\S+ )?\[(?:IPv6:<IP6>|<IP4>)\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$
ignoreregex =

View file

@ -20,8 +20,9 @@ before = common.conf
[Definition]
_daemon = (?:(sm-(mta|acceptingconnections)|sendmail))
__prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )?
prefregex = ^<F-MLFID>%(__prefix_line)s(?:\w{14}: )?</F-MLFID><F-CONTENT>.+</F-CONTENT>$
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[(?:IPv6:<IP6>|<IP4>)\](?: \(may be forged\))?, reject=(550 5\.7\.1 (?P=email)\.\.\. Relaying denied\. (IP name possibly forged \[(\d+\.){3}\d+\]|Proper authentication required\.|IP name lookup failed \[(\d+\.){3}\d+\])|553 5\.1\.8 (?P=email)\.\.\. Domain of sender address \S+ does not exist|550 5\.[71]\.1 (?P=email)\.\.\. (Rejected: .*|User unknown))$
^ruleset=check_relay, arg1=(?P<dom>\S+), arg2=(?:IPv6:<IP6>|<IP4>), relay=((?P=dom) )?\[(\d+\.){3}\d+\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
@ -32,7 +33,7 @@ cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[(?:IP
mdre-normal =
mdre-extra = ^(?:\S+ )?\[(?:IPv6:<IP6>|<IP4>)\](?: \(may be forged\))? did not issue (?:[A-Z]{4}[/ ]?)+during connection to M(?:TA|SP)(?:-\w+)?$
mdre-extra = ^(?:\S+ )?\[(?:IPv6:<IP6>|<IP4>)\](?: \(may be forged\))? did not issue (?:[A-Z]{4}[/ ]?)+during connection to (?:TLS)?M(?:TA|S[PA])(?:-\w+)?$
mdre-aggressive = %(mdre-extra)s
@ -48,7 +49,7 @@ mode = normal
ignoreregex =
journalmatch = _SYSTEMD_UNIT=sendmail.service
journalmatch = SYSLOG_IDENTIFIER=sm-mta + _SYSTEMD_UNIT=sendmail.service
# DEV NOTES:
#

View file

@ -25,7 +25,7 @@ __pref = (?:(?:error|fatal): (?:PAM: )?)?
__suff = (?: (?:port \d+|on \S+|\[preauth\])){0,3}\s*
__on_port_opt = (?: (?:port \d+|on \S+)){0,2}
# close by authenticating user:
__authng_user = (?: authenticating user <F-USER>\S+|.+?</F-USER>)?
__authng_user = (?: (?:invalid|authenticating) user <F-USER>\S+|.+?</F-USER>)?
# for all possible (also future) forms of "no matching (cipher|mac|MAC|compression method|key exchange method|host key type) found",
# see ssherr.c for all possible SSH_ERR_..._ALG_MATCH errors.
@ -57,30 +57,32 @@ cmnfailre = ^[aA]uthentication (?:failure|error|failed) for <F-USER>.*</F-USER>
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+</F-USER> <HOST>%(__on_port_opt)s:\s*Change of username or service not allowed:\s*.*\[preauth\]\s*$
^<F-MLFFORGET>Disconnecting</F-MLFFORGET>: Too many authentication failures(?: for <F-USER>.+?</F-USER>)?%(__suff)s$
^<F-NOFAIL>Received <F-MLFFORGET>disconnect</F-MLFFORGET></F-NOFAIL> from <HOST>%(__on_port_opt)s:\s*11:
^<F-NOFAIL>Connection <F-MLFFORGET>closed</F-MLFFORGET></F-NOFAIL> by%(__authng_user)s <HOST><mdrp-<mode>-suff-onclosed>
<mdre-<mode>-other>
^<F-MLFFORGET><F-MLFGAINED>Accepted \w+</F-MLFGAINED></F-MLFFORGET> for <F-USER>\S+</F-USER> from <HOST>(?:\s|$)
mdre-normal =
# used to differentiate "connection closed" with and without `[preauth]` (fail/nofail cases in ddos mode)
mdrp-normal-suff-onclosed = (?:%(__suff)s|\s*)$
mdre-normal-other = ^<F-NOFAIL><F-MLFFORGET>(Connection closed|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__suff)s|\s*)$
mdre-ddos = ^Did not receive identification string from <HOST>
^Bad protocol version identification '.*' from <HOST>
^Connection <F-MLFFORGET>reset</F-MLFFORGET> by <HOST>
^Connection <F-MLFFORGET>closed</F-MLFFORGET> by%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
^<F-NOFAIL>SSH: Server;Ltype:</F-NOFAIL> (?:Authname|Version|Kex);Remote: <HOST>-\d+;[A-Z]\w+:
^Read from socket failed: Connection <F-MLFFORGET>reset</F-MLFFORGET> by peer
mdrp-ddos-suff-onclosed = %(__on_port_opt)s\s*$
# same as mdre-normal-other, but as failure (without <F-NOFAIL>) and [preauth] only:
mdre-ddos-other = ^<F-MLFFORGET>(Connection closed|Disconnected)</F-MLFFORGET> (?:by|from)%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
mdre-extra = ^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*14: No supported authentication methods available
^Unable to negotiate with <HOST>%(__on_port_opt)s: no matching <__alg_match> found.
^Unable to negotiate a <__alg_match>
^no matching <__alg_match> found:
^<F-MLFFORGET>Disconnected</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+</F-USER> <HOST>%(__on_port_opt)s \[preauth\]\s*$
mdrp-extra-suff-onclosed = %(mdrp-normal-suff-onclosed)s
# part of mdre-ddos-other, but user name is supplied (invalid/authenticating) on [preauth] phase only:
mdre-extra-other = ^<F-MLFFORGET>Disconnected</F-MLFFORGET>(?: from)?(?: (?:invalid|authenticating)) user <F-USER>\S+|.*?</F-USER> <HOST>%(__on_port_opt)s \[preauth\]\s*$
mdre-aggressive = %(mdre-ddos)s
%(mdre-extra)s
mdrp-aggressive-suff-onclosed = %(mdrp-ddos-suff-onclosed)s
# mdre-extra-other is fully included within mdre-ddos-other:
mdre-aggressive-other = %(mdre-ddos-other)s
cfooterre = ^<F-NOFAIL>Connection from</F-NOFAIL> <HOST>
@ -106,8 +108,6 @@ maxlines = 1
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
datepattern = {^LN-BEG}
# DEV Notes:
#
# "Failed \S+ for .*? from <HOST>..." failregex uses non-greedy catch-all because

View file

@ -0,0 +1,56 @@
# Fail2ban filter configuration for traefik :: auth
# used to ban hosts, that were failed through traefik
#
# Author: CrazyMax
#
# To use 'traefik-auth' filter you have to configure your Traefik instance to write
# the access logs as describe in https://docs.traefik.io/configuration/logs/#access-logs
# into a log file on host and specifiy users for Basic Authentication
# https://docs.traefik.io/configuration/entrypoints/#basic-authentication
#
# Example:
#
# version: "3.2"
#
# services:
# traefik:
# image: traefik:latest
# command:
# - "--loglevel=INFO"
# - "--accesslog=true"
# - "--accessLog.filePath=/var/log/access.log"
# # - "--accessLog.filters.statusCodes=400-499"
# - "--defaultentrypoints=http,https"
# - "--entryPoints=Name:http Address::80"
# - "--entryPoints=Name:https Address::443 TLS"
# - "--docker.domain=example.com"
# - "--docker.watch=true"
# - "--docker.exposedbydefault=false"
# - "--api=true"
# - "--api.dashboard=true"
# ports:
# - target: 80
# published: 80
# protocol: tcp
# mode: host
# - target: 443
# published: 443
# protocol: tcp
# mode: host
# labels:
# - "traefik.enable=true"
# - "traefik.port=8080"
# - "traefik.backend=traefik"
# - "traefik.frontend.rule=Host:traefik.example.com"
# - "traefik.frontend.auth.basic.users=test:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/"
# volumes:
# - "/var/log/traefik:/var/log"
# - "/var/run/docker.sock:/var/run/docker.sock"
# restart: always
#
[Definition]
failregex = ^<HOST> \- (?!- )\S+ \[\] \"(GET|POST|HEAD) [^\"]+\" 401\b
ignoreregex =

View file

@ -0,0 +1,34 @@
# Fail2Ban filter for ZNC (requires adminlog module)
#
# to use this module, enable the adminlog module from within ZNC and point
# logpath to its logfile (e.g. /var/lib/znc/moddata/adminlog/znc.log).
[DEFAULT]
logtype = file
[Definition]
_daemon = znc
# Prefix for different logtype (file, journal):
#
__prefix_file = (?:\[\]\s+)?
__prefix_short = (?:\S+\s+%(_daemon)s\[\d+\]:)\s+
__prefix_journal = %(__prefix_short)s
__prefix_line = <__prefix_<logtype>>
failregex = ^%(__prefix_line)s\[[^]]+\] failed to login from <ADDR>
ignoreregex =
journalmatch = _SYSTEMD_UNIT=znc.service + _COMM=znc
# DEV Notes:
# Log format is: [<DATE+TIME>] [<USERNAME>] <ACTION> from <ADDR>
# [2018-10-27 01:40:17] [girst] connected to ZNC from 1.2.3.4
# [2018-10-27 01:40:21] [girst] disconnected from ZNC from 1.2.3.4
# [2018-10-27 01:40:55] [girst] failed to login from 1.2.3.4
#
# Author: Tobias Girstmair (//gir.st/)

View file

@ -44,6 +44,44 @@ before = paths-debian.conf
# MISCELLANEOUS OPTIONS
#
# "bantime.increment" allows to use database for searching of previously banned ip's to increase a
# default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
#bantime.increment = true
# "bantime.rndtime" is the max number of seconds using for mixing with random time
# to prevent "clever" botnets calculate exact time IP can be unbanned again:
#bantime.rndtime =
# "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
#bantime.maxtime =
# "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
# default value of factor is 1 and with default value of formula, the ban time
# grows by 1, 2, 4, 8, 16 ...
#bantime.factor = 1
# "bantime.formula" used by default to calculate next value of ban time, default value bellow,
# the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
#bantime.formula = ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor
#
# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" :
#bantime.formula = ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)
# "bantime.multipliers" used to calculate next value of ban time instead of formula, coresponding
# previously ban count and given "bantime.factor" (for multipliers default is 1);
# following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
# always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
#bantime.multipliers = 1 2 4 8 16 32 64
# following example can be used for small initial ban time (bantime=60) - it grows more aggressive at begin,
# for bantime=60 the multipliers are minutes and equal: 1 min, 5 min, 30 min, 1 hour, 5 hour, 12 hour, 1 day, 2 day
#bantime.multipliers = 1 5 30 60 300 720 1440 2880
# "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
# cross over all jails, if false (dafault), only current jail of the ban IP will be searched
#bantime.overalljails = false
# --------------------
# "ignoreself" specifies whether the local resp. own IP addresses should be ignored
# (default is true). Fail2ban will not ban a host which matches such addresses.
#ignoreself = true
@ -69,6 +107,9 @@ findtime = 10m
# "maxretry" is the number of failures before a host get banned.
maxretry = 5
# "maxmatches" is the number of matches stored in ticket (resolvable via tag <matches> in actions).
maxmatches = %(maxretry)s
# "backend" specifies the backend used to get files modification.
# Available options are "pyinotify", "gamin", "polling", "systemd" and "auto".
# This option can be overridden in each jail as well.
@ -168,22 +209,22 @@ banaction = iptables-multiport
banaction_allports = iptables-allports
# The simplest action to take: ban only
action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_ = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report to the destemail.
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]
# ban & send an e-mail with whois report and relevant log lines
# to the destemail.
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mwl = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath="%(logpath)s", chain="%(chain)s"]
# See the IMPORTANT note in action.d/xarf-login-attack for when to use this action
#
# ban & send a xarf e-mail to abuse contact of IP address and include relevant log lines
# to the destemail.
action_xarf = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_xarf = %(banaction)s[name=%(__name__)s, port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
@ -442,6 +483,7 @@ logpath = /var/log/tomcat*/catalina.out
#Ban clients brute-forcing the monit gui login
port = 2812
logpath = /var/log/monit
/var/log/monit.log
[webmin-auth]
@ -731,9 +773,16 @@ logpath = /var/log/freeswitch.log
maxretry = 10
# enable adminlog; it will log to a file inside znc's directory by default.
[znc-adminlog]
port = 6667
logpath = /var/lib/znc/moddata/adminlog/znc.log
# To log wrong MySQL access attempts add to /etc/my.cnf in [mysqld] or
# equivalent section:
# log-warning = 2
# log-warnings = 2
#
# for syslog (daemon facility)
# [mysqld_safe]
@ -810,6 +859,14 @@ udpport = 1200,27000,27001,27002,27003,27004,27005,27006,27007,27008,27009,27010
action = %(banaction)s[name=%(__name__)s-tcp, port="%(tcpport)s", protocol="tcp", chain="%(chain)s", actname=%(banaction)s-tcp]
%(banaction)s[name=%(__name__)s-udp, port="%(udpport)s", protocol="udp", chain="%(chain)s", actname=%(banaction)s-udp]
[bitwarden]
port = http,https
logpath = /home/*/bwdata/logs/identity/Identity/log.txt
[centreon]
port = http,https
logpath = /var/log/centreon/login.log
# consider low maxretry and a long bantime
# nobody except your own Nagios server should ever probe nrpe
[nagios]
@ -842,7 +899,8 @@ filter = apache-pass[knocking_url="%(knocking_url)s"]
logpath = %(apache_access_log)s
blocktype = RETURN
returntype = DROP
action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s]
action = %(action_)s[blocktype=%(blocktype)s, returntype=%(returntype)s,
actionstart_on_demand=false, actionrepair_on_unban=true]
bantime = 1h
maxretry = 1
findtime = 1
@ -888,3 +946,8 @@ backend = %(syslog_backend)s
port = http,https
logpath = %(apache_error_log)s
[traefik-auth]
# to use 'traefik-auth' filter you have to configure your Traefik instance,
# see `filter.d/traefik-auth.conf` for details and service example.
port = http,https
logpath = /var/log/traefik/access.log

View file

@ -23,8 +23,6 @@ syslog_daemon = /var/log/daemon.log
exim_main_log = /var/log/exim4/mainlog
dovecot_log = %(syslog_mail)s
# was in debian squeezy but not in wheezy
# /etc/proftpd/proftpd.conf (SystemLog)
proftpd_log = /var/log/proftpd/proftpd.log

View file

@ -44,7 +44,10 @@ class ActionReader(DefinitionInitConfigReader):
"actionreload": ["string", None],
"actioncheck": ["string", None],
"actionrepair": ["string", None],
"actionrepair_on_unban": ["string", None],
"actionban": ["string", None],
"actionprolong": ["string", None],
"actionreban": ["string", None],
"actionunban": ["string", None],
"norestored": ["string", None],
}
@ -78,7 +81,7 @@ class ActionReader(DefinitionInitConfigReader):
opts = self.getCombined(
ignore=CommandAction._escapedTags | set(('timeout', 'bantime')))
# type-convert only after combined (otherwise boolean converting prevents substitution):
for o in ('norestored', 'actionstart_on_demand'):
for o in ('norestored', 'actionstart_on_demand', 'actionrepair_on_unban'):
if opts.get(o):
opts[o] = self._convert_to_boolean(opts[o])

View file

@ -180,6 +180,12 @@ class Beautifier:
msg = "The jail %s action %s has the following " \
"methods:\n" % (inC[1], inC[3])
msg += ", ".join(response)
elif inC[2] == "banip" and inC[0] == "get":
if isinstance(response, list):
sep = " " if len(inC) <= 3 else inC[3]
if sep == "--with-time":
sep = "\n"
msg = sep.join(response)
except Exception:
logSys.warning("Beautifier error. Please report the error")
logSys.error("Beautify %r with %r failed", response, self.__inputCmd,

View file

@ -120,6 +120,9 @@ class ConfigReader():
except AttributeError:
return False
def merge_defaults(self, d):
self._cfg.get_defaults().update(d)
def merge_section(self, section, *args, **kwargs):
try:
return self._cfg.merge_section(section, *args, **kwargs)
@ -239,8 +242,10 @@ class ConfigReaderUnshared(SafeConfigParserWithIncludes):
try:
if opttype == "bool":
v = self.getboolean(sec, optname)
if v is None: continue
elif opttype == "int":
v = self.getint(sec, optname)
if v is None: continue
else:
v = self.get(sec, optname, vars=pOptions)
values[optname] = v

View file

@ -186,8 +186,7 @@ class Fail2banClient(Fail2banCmdLine, Thread):
logSys.error("Fail2ban seems to be in unexpected state (not running but the socket exists)")
return None
stream.append(['server-status'])
return stream
return [["server-stream", stream], ['server-status']]
##
def __startServer(self, background=True):

View file

@ -54,19 +54,27 @@ class Fail2banReader(ConfigReader):
["string", "logtarget", "STDERR"],
["string", "syslogsocket", "auto"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["int", "dbmaxmatches", None],
["string", "dbpurgeage", "1d"]]
self.__opts = ConfigReader.getOptions(self, "Definition", opts)
if updateMainOpt:
self.__opts.update(updateMainOpt)
# check given log-level:
str2LogLevel(self.__opts.get('loglevel', 0))
# thread options:
opts = [["int", "stacksize", ],
]
if self.has_section("Thread"):
thopt = ConfigReader.getOptions(self, "Thread", opts)
if thopt:
self.__opts['thread'] = thopt
def convert(self):
# Ensure logtarget/level set first so any db errors are captured
# Also dbfile should be set before all other database options.
# So adding order indices into items, to be stripped after sorting, upon return
order = {"syslogsocket":0, "loglevel":1, "logtarget":2,
"dbfile":50, "dbpurgeage":51}
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
stream = list()
for opt in self.__opts:
if opt in order:

View file

@ -25,7 +25,13 @@ This tools can test regular expressions for "fail2ban".
"""
__author__ = "Fail2Ban Developers"
__copyright__ = "Copyright (c) 2004-2008 Cyril Jaquier, 2012-2014 Yaroslav Halchenko"
__copyright__ = """Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors
Copyright of modifications held by their respective authors.
Licensed under the GNU General Public License v2 (GPL).
Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>.
Many contributions by Yaroslav O. Halchenko, Steven Hiscocks, Sergey G. Brester (sebres)."""
__license__ = "GPL"
import getopt
@ -97,11 +103,13 @@ def dumpNormVersion(*args):
output(normVersion())
sys.exit(0)
def get_opt_parser():
# use module docstring for help output
p = OptionParser(
usage="%s [OPTIONS] <LOG> <REGEX> [IGNOREREGEX]\n" % sys.argv[0] + __doc__
+ """
usage = lambda: "%s [OPTIONS] <LOG> <REGEX> [IGNOREREGEX]" % sys.argv[0]
class _f2bOptParser(OptionParser):
def format_help(self, *args, **kwargs):
""" Overwritten format helper with full ussage."""
self.usage = ''
return "Usage: " + usage() + __doc__ + """
LOG:
string a string representing a log line
filename path to a log file (/var/log/auth.log)
@ -114,16 +122,15 @@ REGEX:
IGNOREREGEX:
string a string representing an 'ignoreregex'
filename path to a filter file (filter.d/sshd.conf)
\n""" + OptionParser.format_help(self, *args, **kwargs) + """\n
Report bugs to https://github.com/fail2ban/fail2ban/issues\n
""" + __copyright__ + "\n"
Copyright (c) 2004-2008 Cyril Jaquier, 2008- Fail2Ban Contributors
Copyright of modifications held by their respective authors.
Licensed under the GNU General Public License v2 (GPL).
Written by Cyril Jaquier <cyril.jaquier@fail2ban.org>.
Many contributions by Yaroslav O. Halchenko and Steven Hiscocks.
Report bugs to https://github.com/fail2ban/fail2ban/issues
""",
def get_opt_parser():
# use module docstring for help output
p = _f2bOptParser(
usage=usage(),
version="%prog " + version)
p.add_options([
@ -160,6 +167,10 @@ Report bugs to https://github.com/fail2ban/fail2ban/issues
help="Verbose date patterns/regex in output"),
Option("-D", "--debuggex", action='store_true',
help="Produce debuggex.com urls for debugging there"),
Option("--no-check-all", action="store_false", dest="checkAllRegex", default=True,
help="Disable check for all regex's"),
Option("-o", "--out", action="store", dest="out", default=None,
help="Set token to print failure information only (row, id, ip, msg, host, ip4, ip6, dns, matches, ...)"),
Option("--print-no-missed", action='store_true',
help="Do not print any missed lines"),
Option("--print-no-ignored", action='store_true',
@ -234,6 +245,7 @@ class Fail2banRegex(object):
def __init__(self, opts):
# set local protected members from given options:
self.__dict__.update(dict(('_'+o,v) for o,v in opts.__dict__.iteritems()))
self._opts = opts
self._maxlines_set = False # so we allow to override maxlines in cmdline
self._datepattern_set = False
self._journalmatch = None
@ -259,8 +271,11 @@ class Fail2banRegex(object):
self._filter.setUseDns(opts.usedns)
self._filter.returnRawHost = opts.raw
self._filter.checkFindTime = False
self._filter.checkAllRegex = True
self._opts = opts
self._filter.checkAllRegex = opts.checkAllRegex and not opts.out
self._backend = 'auto'
def output(self, line):
if not self._opts.out: output(line)
def decode_line(self, line):
return FileContainer.decode_line('<LOG>', self._encoding, line)
@ -273,18 +288,31 @@ class Fail2banRegex(object):
self._filter.setDatePattern(pattern)
self._datepattern_set = True
if pattern is not None:
output( "Use datepattern : %s" % (
self.output( "Use datepattern : %s" % (
self._filter.getDatePattern()[1], ) )
def setMaxLines(self, v):
if not self._maxlines_set:
self._filter.setMaxLines(int(v))
self._maxlines_set = True
output( "Use maxlines : %d" % self._filter.getMaxLines() )
self.output( "Use maxlines : %d" % self._filter.getMaxLines() )
def setJournalMatch(self, v):
self._journalmatch = v
def _dumpRealOptions(self, reader, fltOpt):
realopts = {}
combopts = reader.getCombined()
# output all options that are specified in filter-argument as well as some special (mostly interested):
for k in ['logtype', 'datepattern'] + fltOpt.keys():
# combined options win, but they contain only a sub-set in filter expected keys,
# so get the rest from definition section:
try:
realopts[k] = combopts[k] if k in combopts else reader.get('Definition', k)
except NoOptionError: # pragma: no cover
pass
self.output("Real filter options : %r" % realopts)
def readRegex(self, value, regextype):
assert(regextype in ('fail', 'ignore'))
regex = regextype + 'regex'
@ -320,13 +348,15 @@ class Fail2banRegex(object):
if os.path.basename(basedir) == 'filter.d':
basedir = os.path.dirname(basedir)
fltName = os.path.splitext(os.path.basename(fltName))[0]
output( "Use %11s filter file : %s, basedir: %s" % (regex, fltName, basedir) )
self.output( "Use %11s filter file : %s, basedir: %s" % (regex, fltName, basedir) )
else:
## foreign file - readexplicit this file and includes if possible:
output( "Use %11s file : %s" % (regex, fltName) )
self.output( "Use %11s file : %s" % (regex, fltName) )
basedir = None
if not os.path.isabs(fltName): # avoid join with "filter.d" inside FilterReader
fltName = os.path.abspath(fltName)
if fltOpt:
output( "Use filter options : %r" % fltOpt )
self.output( "Use filter options : %r" % fltOpt )
reader = FilterReader(fltName, 'fail2ban-regex-jail', fltOpt, share_config=self.share_config, basedir=basedir)
ret = None
try:
@ -342,7 +372,17 @@ class Fail2banRegex(object):
if not ret:
output( "ERROR: failed to load filter %s" % value )
return False
# overwrite default logtype (considering that the filter could specify this too in Definition/Init sections):
if not fltOpt.get('logtype'):
reader.merge_defaults({
'logtype': ['file','journal'][int(self._backend.startswith("systemd"))]
})
# get, interpolate and convert options:
reader.getOptions(None)
# show real options if expected:
if self._verbose > 1 or logSys.getEffectiveLevel()<=logging.DEBUG:
self._dumpRealOptions(reader, fltOpt)
# to stream:
readercommands = reader.convert()
regex_values = {}
@ -384,7 +424,7 @@ class Fail2banRegex(object):
return False
else:
output( "Use %11s line : %s" % (regex, shortstr(value)) )
self.output( "Use %11s line : %s" % (regex, shortstr(value)) )
regex_values = {regextype: [RegexStat(value)]}
for regextype, regex_values in regex_values.iteritems():
@ -480,6 +520,20 @@ class Fail2banRegex(object):
if len(ret) > 0:
assert(not is_ignored)
if self._opts.out:
if self._opts.out in ('id', 'ip'):
for ret in ret:
output(ret[1])
elif self._opts.out == 'msg':
for ret in ret:
output('\n'.join(map(lambda v:''.join(v for v in v), ret[3].get('matches'))))
elif self._opts.out == 'row':
for ret in ret:
output('[%r,\t%r,\t%r],' % (ret[1],ret[2],dict((k,v) for k, v in ret[3].iteritems() if k != 'matches')))
else:
for ret in ret:
output(ret[3].get(self._opts.out))
continue
self._line_stats.matched += 1
if self._print_all_matched:
self._line_stats.matched_lines.append(line)
@ -528,6 +582,7 @@ class Fail2banRegex(object):
"to print all %d lines" % (header, ltype, lines) )
def printStats(self):
if self._opts.out: return True
output( "" )
output( "Results" )
output( "=======" )
@ -595,6 +650,9 @@ class Fail2banRegex(object):
cmd_log, cmd_regex = args[:2]
if cmd_log.startswith("systemd-journal"): # pragma: no cover
self._backend = 'systemd'
try:
if not self.readRegex(cmd_regex, 'fail'): # pragma: no cover
return False
@ -607,8 +665,8 @@ class Fail2banRegex(object):
if os.path.isfile(cmd_log):
try:
hdlr = open(cmd_log, 'rb')
output( "Use log file : %s" % cmd_log )
output( "Use encoding : %s" % self._encoding )
self.output( "Use log file : %s" % cmd_log )
self.output( "Use encoding : %s" % self._encoding )
test_lines = self.file_lines_gen(hdlr)
except IOError as e: # pragma: no cover
output( e )
@ -617,8 +675,8 @@ class Fail2banRegex(object):
if not FilterSystemd:
output( "Error: systemd library not found. Exiting..." )
return False
output( "Use systemd journal" )
output( "Use encoding : %s" % self._encoding )
self.output( "Use systemd journal" )
self.output( "Use encoding : %s" % self._encoding )
backend, beArgs = extractOptions(cmd_log)
flt = FilterSystemd(None, **beArgs)
flt.setLogEncoding(self._encoding)
@ -627,23 +685,23 @@ class Fail2banRegex(object):
self.setDatePattern(None)
if journalmatch:
flt.addJournalMatch(journalmatch)
output( "Use journal match : %s" % " ".join(journalmatch) )
self.output( "Use journal match : %s" % " ".join(journalmatch) )
test_lines = journal_lines_gen(flt, myjournal)
else:
# if single line parsing (without buffering)
if self._filter.getMaxLines() <= 1:
output( "Use single line : %s" % shortstr(cmd_log.replace("\n", r"\n")) )
self.output( "Use single line : %s" % shortstr(cmd_log.replace("\n", r"\n")) )
test_lines = [ cmd_log ]
else: # multi line parsing (with buffering)
test_lines = cmd_log.split("\n")
output( "Use multi line : %s line(s)" % len(test_lines) )
self.output( "Use multi line : %s line(s)" % len(test_lines) )
for i, l in enumerate(test_lines):
if i >= 5:
output( "| ..." ); break
output( "| %2.2s: %s" % (i+1, shortstr(l)) )
output( "`-" )
self.output( "| ..." ); break
self.output( "| %2.2s: %s" % (i+1, shortstr(l)) )
self.output( "`-" )
output( "" )
self.output( "" )
self.process(test_lines)
@ -666,14 +724,15 @@ def exec_command_line(*args):
if not len(args) in (2, 3):
errors.append("ERROR: provide both <LOG> and <REGEX>.")
if errors:
sys.stderr.write("\n".join(errors) + "\n\n")
parser.print_help()
sys.stderr.write("\n" + "\n".join(errors) + "\n")
sys.exit(255)
output( "" )
output( "Running tests" )
output( "=============" )
output( "" )
if not opts.out:
output( "" )
output( "Running tests" )
output( "=============" )
output( "" )
# Log level (default critical):
opts.log_level = str2LogLevel(opts.log_level)
@ -694,6 +753,14 @@ def exec_command_line(*args):
stdout.setFormatter(Formatter(getVerbosityFormat(opts.verbose, fmt)))
logSys.addHandler(stdout)
fail2banRegex = Fail2banRegex(opts)
try:
fail2banRegex = Fail2banRegex(opts)
except Exception as e:
if opts.verbose or logSys.getEffectiveLevel()<=logging.DEBUG:
logSys.critical(e, exc_info=True)
else:
output( 'ERROR: %s' % e )
sys.exit(255)
if not fail2banRegex.start(args):
sys.exit(255)

View file

@ -37,9 +37,10 @@ logSys = getLogger(__name__)
class FilterReader(DefinitionInitConfigReader):
_configOpts = {
"usedns": ["string", None],
"prefregex": ["string", None],
"ignoreregex": ["string", None],
"failregex": ["string", ""],
"failregex": ["string", None],
"maxlines": ["int", None],
"datepattern": ["string", None],
"journalmatch": ["string", None],
@ -57,6 +58,11 @@ class FilterReader(DefinitionInitConfigReader):
opts = self.getCombined()
if not len(opts):
return stream
return FilterReader._fillStream(stream, opts, self._jailName)
@staticmethod
def _fillStream(stream, opts, jailName):
prio0idx = 0
for opt, value in opts.iteritems():
if opt in ("failregex", "ignoreregex"):
if value is None: continue
@ -66,21 +72,22 @@ class FilterReader(DefinitionInitConfigReader):
if regex != '':
multi.append(regex)
if len(multi) > 1:
stream.append(["multi-set", self._jailName, "add" + opt, multi])
stream.append(["multi-set", jailName, "add" + opt, multi])
elif len(multi):
stream.append(["set", self._jailName, "add" + opt, multi[0]])
elif opt in ('maxlines', 'prefregex'):
# Be sure we set this options first.
stream.insert(0, ["set", self._jailName, opt, value])
stream.append(["set", jailName, "add" + opt, multi[0]])
elif opt in ('usedns', 'maxlines', 'prefregex'):
# Be sure we set this options first, and usedns is before all regex(s).
stream.insert(0 if opt == 'usedns' else prio0idx,
["set", jailName, opt, value])
prio0idx += 1
elif opt in ('datepattern'):
stream.append(["set", self._jailName, opt, value])
# Do not send a command if the match is empty.
stream.append(["set", jailName, opt, value])
elif opt == 'journalmatch':
# Do not send a command if the match is empty.
if value is None: continue
for match in value.split("\n"):
if match == '': continue
stream.append(
["set", self._jailName, "addjournalmatch"] +
shlex.split(match))
["set", jailName, "addjournalmatch"] + shlex.split(match))
return stream

View file

@ -33,7 +33,7 @@ from .configreader import ConfigReaderUnshared, ConfigReader
from .filterreader import FilterReader
from .actionreader import ActionReader
from ..version import version
from ..helpers import getLogger, extractOptions, splitwords
from ..helpers import getLogger, extractOptions, splitWithOptions, splitwords
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -86,36 +86,51 @@ class JailReader(ConfigReader):
logSys.warning("File %s is a dangling link, thus cannot be monitored" % p)
return pathList
_configOpts1st = {
"enabled": ["bool", False],
"backend": ["string", "auto"],
"filter": ["string", ""]
}
_configOpts = {
"enabled": ["bool", False],
"backend": ["string", "auto"],
"maxretry": ["int", None],
"maxmatches": ["int", None],
"findtime": ["string", None],
"bantime": ["string", None],
"bantime.increment": ["bool", None],
"bantime.factor": ["string", None],
"bantime.formula": ["string", None],
"bantime.multipliers": ["string", None],
"bantime.maxtime": ["string", None],
"bantime.rndtime": ["string", None],
"bantime.overalljails": ["bool", None],
"ignorecommand": ["string", None],
"ignoreself": ["bool", None],
"ignoreip": ["string", None],
"ignorecache": ["string", None],
"filter": ["string", ""],
"logtimezone": ["string", None],
"logencoding": ["string", None],
"logpath": ["string", None],
"action": ["string", ""]
}
_configOpts.update(FilterReader._configOpts)
_ignoreOpts = set(['action', 'filter', 'enabled'] + FilterReader._configOpts.keys())
def getOptions(self):
opts1st = [["bool", "enabled", False],
["string", "filter", ""]]
opts = [["bool", "enabled", False],
["string", "backend", "auto"],
["int", "maxretry", None],
["string", "findtime", None],
["string", "bantime", None],
["string", "usedns", None], # be sure usedns is before all regex(s) in stream
["string", "failregex", None],
["string", "ignoreregex", None],
["string", "ignorecommand", None],
["bool", "ignoreself", None],
["string", "ignoreip", None],
["string", "ignorecache", None],
["string", "filter", ""],
["string", "datepattern", None],
["string", "logtimezone", None],
["string", "logencoding", None],
["string", "logpath", None], # logpath after all log-related data (backend, date-pattern, etc)
["string", "action", ""]]
# Before interpolation (substitution) add static options always available as default:
defsec = self._cfg.get_defaults()
defsec["fail2ban_version"] = version
self.merge_defaults({
"fail2ban_version": version
})
try:
# Read first options only needed for merge defaults ('known/...' from filter):
self.__opts = ConfigReader.getOptions(self, self.__name, opts1st, shouldExist=True)
self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts1st,
shouldExist=True)
if not self.__opts: # pragma: no cover
raise JailDefError("Init jail options failed")
@ -134,6 +149,11 @@ class JailReader(ConfigReader):
ret = self.__filter.read()
if not ret:
raise JailDefError("Unable to read the filter %r" % filterName)
if not filterOpt.get('logtype'):
# overwrite default logtype backend-related (considering that the filter settings may be overwritten):
self.__filter.merge_defaults({
'logtype': ['file','journal'][int(self.__opts.get('backend', '').startswith("systemd"))]
})
# merge options from filter as 'known/...' (all options unfiltered):
self.__filter.getOptions(self.__opts, all=True)
ConfigReader.merge_section(self, self.__name, self.__filter.getCombined(), 'known/')
@ -142,7 +162,7 @@ class JailReader(ConfigReader):
logSys.warning("No filter set for jail %s" % self.__name)
# Read second all options (so variables like %(known/param) can be interpolated):
self.__opts = ConfigReader.getOptions(self, self.__name, opts)
self.__opts = ConfigReader.getOptions(self, self.__name, self._configOpts)
if not self.__opts: # pragma: no cover
raise JailDefError("Read jail options failed")
@ -151,21 +171,15 @@ class JailReader(ConfigReader):
self.__filter.getOptions(self.__opts)
# Read action
prevln = ''
actlst = self.__opts["action"].split('\n')
for n, act in enumerate(actlst):
for act in splitWithOptions(self.__opts["action"]):
try:
act = act.strip()
if not act: # skip empty actions
continue
# join with previous line if needed (consider possible new-line):
if prevln: act = prevln + '\n' + act
actName, actOpt = extractOptions(act)
prevln = ''
if not actName:
# consider possible new-line, so repeat with joined next line's:
if n < len(actlst) - 1:
prevln = act
continue
raise JailDefError("Invalid action definition %r" % act)
if actName.endswith(".py"):
self.__actions.append([
@ -215,15 +229,19 @@ class JailReader(ConfigReader):
"""
stream = []
stream2 = []
e = self.__opts.get('config-error')
if e:
stream.extend([['config-error', "Jail '%s' skipped, because of wrong configuration: %s" % (self.__name, e)]])
return stream
# fill jail with filter options, using filter (only not overriden in jail):
if self.__filter:
stream.extend(self.__filter.convert())
# and using options from jail:
FilterReader._fillStream(stream, self.__opts, self.__name)
for opt, value in self.__opts.iteritems():
if opt == "logpath":
if self.__opts.get('backend', None).startswith("systemd"): continue
if self.__opts.get('backend', '').startswith("systemd"): continue
found_files = 0
for path in value.split("\n"):
path = path.rsplit(" ", 1)
@ -233,33 +251,22 @@ class JailReader(ConfigReader):
logSys.notice("No file(s) found for glob %s" % path)
for p in pathList:
found_files += 1
stream.append(
# logpath after all log-related data (backend, date-pattern, etc)
stream2.append(
["set", self.__name, "addlogpath", p, tail])
if not found_files:
msg = "Have not found any log file for %s jail" % self.__name
if not allow_no_files:
raise ValueError(msg)
logSys.warning(msg)
elif opt == "logencoding":
stream.append(["set", self.__name, "logencoding", value])
elif opt == "backend":
backend = value
elif opt == "ignoreip":
for ip in splitwords(value):
stream.append(["set", self.__name, "addignoreip", ip])
elif opt in ("failregex", "ignoreregex"):
multi = []
for regex in value.split('\n'):
# Do not send a command if the rule is empty.
if regex != '':
multi.append(regex)
if len(multi) > 1:
stream.append(["multi-set", self.__name, "add" + opt, multi])
elif len(multi):
stream.append(["set", self.__name, "add" + opt, multi[0]])
elif opt not in ('action', 'filter', 'enabled'):
stream.append(["set", self.__name, "addignoreip"] + splitwords(value))
elif opt not in JailReader._ignoreOpts:
stream.append(["set", self.__name, opt, value])
# consider options order (after other options):
if stream2: stream += stream2
for action in self.__actions:
if isinstance(action, (ConfigReaderUnshared, ConfigReader)):
stream.extend(action.convert())

View file

@ -32,6 +32,13 @@ from threading import Lock
from .server.mytime import MyTime
try:
import ctypes
_libcap = ctypes.CDLL('libcap.so.2')
except:
_libcap = None
PREFER_ENC = locale.getpreferredencoding()
# correct preferred encoding if lang not set in environment:
if PREFER_ENC.startswith('ANSI_'): # pragma: no cover
@ -329,6 +336,9 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
# `action = act[p1=...][p2=...]`
OPTION_EXTRACT_CRE = re.compile(
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL)
# split by new-line considering possible new-lines within options [...]:
OPTION_SPLIT_CRE = re.compile(
r'(?:[^\[\n]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|[^\n]+)(?=\n\s*|$)', re.DOTALL)
def extractOptions(option):
match = OPTION_CRE.match(option)
@ -345,6 +355,9 @@ def extractOptions(option):
option_opts[opt.strip()] = value.strip()
return option_name, option_opts
def splitWithOptions(option):
return OPTION_SPLIT_CRE.findall(option)
#
# Following facilities used for safe recursive interpolation of
# tags (<tag>) in tagged options.
@ -379,8 +392,7 @@ def substituteRecursiveTags(inptags, conditional='',
"""
#logSys = getLogger("fail2ban")
tre_search = TAG_CRE.search
# copy return tags dict to prevent modifying of inptags:
tags = inptags.copy()
tags = inptags
# init:
ignore = set(ignore)
done = set()
@ -442,6 +454,9 @@ def substituteRecursiveTags(inptags, conditional='',
# check still contains any tag - should be repeated (possible embedded-recursive substitution):
if tre_search(value):
repFlag = True
# copy return tags dict to prevent modifying of inptags:
if id(tags) == id(inptags):
tags = inptags.copy()
tags[tag] = value
# no more sub tags (and no possible composite), add this tag to done set (just to be faster):
if '<' not in value: done.add(tag)
@ -451,6 +466,25 @@ def substituteRecursiveTags(inptags, conditional='',
return tags
if _libcap:
def prctl_set_th_name(name):
"""Helper to set real thread name (used for identification and diagnostic purposes).
Side effect: name can be silently truncated to 15 bytes (16 bytes with NTS zero)
"""
try:
if sys.version_info >= (3,): # pragma: 2.x no cover
name = name.encode()
else: # pragma: 3.x no cover
name = bytes(name)
_libcap.prctl(15, name) # PR_SET_NAME = 15
except: # pragma: no cover
pass
else: # pragma: no cover
def prctl_set_th_name(name):
pass
class BgService(object):
"""Background servicing

View file

@ -72,6 +72,8 @@ protocol = [
['', "DATABASE", ""],
["set dbfile <FILE>", "set the location of fail2ban persistent datastore. Set to \"None\" to disable"],
["get dbfile", "get the location of fail2ban persistent datastore"],
["set dbmaxmatches <INT>", "sets the max number of matches stored in database per ticket"],
["get dbmaxmatches", "gets the max number of matches stored in database per ticket"],
["set dbpurgeage <SECONDS>", "sets the max age in <SECONDS> that history of bans will be kept"],
["get dbpurgeage", "gets the max age in seconds that history of bans will be kept"],
['', "JAIL CONTROL", ""],
@ -99,9 +101,11 @@ protocol = [
["set <JAIL> bantime <TIME>", "sets the number of seconds <TIME> a host will be banned for <JAIL>"],
["set <JAIL> datepattern <PATTERN>", "sets the <PATTERN> used to match date/times for <JAIL>"],
["set <JAIL> usedns <VALUE>", "sets the usedns mode for <JAIL>"],
["set <JAIL> banip <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> attempt <IP> [<failure1> ... <failureN>]", "manually notify about <IP> failure"],
["set <JAIL> banip <IP> ... <IP>", "manually Ban <IP> for <JAIL>"],
["set <JAIL> unbanip [--report-absent] <IP> ... <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxmatches <INT>", "sets the max number of matches stored in memory per ticket in <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <ACT> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"],
["set <JAIL> delaction <ACT>", "removes the action <ACT> from <JAIL>"],
@ -128,7 +132,9 @@ protocol = [
["get <JAIL> bantime", "gets the time a host is banned for <JAIL>"],
["get <JAIL> datepattern", "gets the patern used to match date/times for <JAIL>"],
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
["get <JAIL> banip [<SEP>|--with-time]", "gets the list of of banned IP addresses for <JAIL>. Optionally the separator character ('<SEP>', default is space) or the option '--with-time' (printing the times of ban) may be specified. The IPs are ordered by end of ban."],
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
["get <JAIL> maxmatches", "gets the max number of matches stored in memory per ticket in <JAIL>"],
["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"],
["get <JAIL> actions", "gets a list of actions for <JAIL>"],
["", "COMMAND ACTION INFORMATION",""],

View file

@ -33,10 +33,11 @@ from abc import ABCMeta
from collections import MutableMapping
from .failregex import mapTag2Opt
from .ipdns import asip, DNSUtils
from .ipdns import DNSUtils
from .mytime import MyTime
from .utils import Utils
from ..helpers import getLogger, _merge_copy_dicts, uni_string, substituteRecursiveTags, TAG_CRE, MAX_TAG_REPLACE_COUNT
from ..helpers import getLogger, _merge_copy_dicts, \
splitwords, substituteRecursiveTags, uni_string, TAG_CRE, MAX_TAG_REPLACE_COUNT
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -44,13 +45,14 @@ logSys = getLogger(__name__)
# Create a lock for running system commands
_cmd_lock = threading.Lock()
# Todo: make it configurable resp. automatically set, ex.: `[ -f /proc/net/if_inet6 ] && echo 'yes' || echo 'no'`:
allowed_ipv6 = True
# Specifies whether IPv6 subsystem is available:
allowed_ipv6 = DNSUtils.IPv6IsAllowed
# capture groups from filter for map to ticket data:
FCUSTAG_CRE = re.compile(r'<F-([A-Z0-9_\-]+)>'); # currently uppercase only
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=")
COND_FAMILIES = ('inet4', 'inet6')
CONDITIONAL_FAM_RE = re.compile(r"^(\w+)\?(family)=(.*)$")
# Special tags:
DYN_REPL_TAGS = {
@ -173,7 +175,7 @@ class CallingMap(MutableMapping, object):
def __len__(self):
return len(self.data)
def copy(self): # pragma: no cover
def copy(self):
return self.__class__(_merge_copy_dicts(self.data, self.storage))
@ -215,6 +217,7 @@ class ActionBase(object):
"start",
"stop",
"ban",
"reban",
"unban",
)
for method in required:
@ -248,6 +251,21 @@ class ActionBase(object):
"""
pass
def reban(self, aInfo): # pragma: no cover - abstract
"""Executed when a ban occurs.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
return self.ban(aInfo)
@property
def _prolongable(self): # pragma: no cover - abstract
return False
def unban(self, aInfo): # pragma: no cover - abstract
"""Executed when a ban expires.
@ -260,6 +278,11 @@ class ActionBase(object):
pass
WRAP_CMD_PARAMS = {
'timeout': 'str2seconds',
'bantime': 'ignore',
}
class CommandAction(ActionBase):
"""A action which executes OS shell commands.
@ -279,6 +302,7 @@ class CommandAction(ActionBase):
----------
actionban
actioncheck
actionreban
actionreload
actionrepair
actionstart
@ -299,6 +323,7 @@ class CommandAction(ActionBase):
self.actionstart = ''
## Command executed when ticket gets banned.
self.actionban = ''
self.actionreban = ''
## Command executed when ticket gets removed.
self.actionunban = ''
## Command executed in order to check requirements.
@ -330,7 +355,10 @@ class CommandAction(ActionBase):
def __setattr__(self, name, value):
if not name.startswith('_') and not self.__init and not callable(value):
# special case for some parameters:
if name in ('timeout', 'bantime'):
wrp = WRAP_CMD_PARAMS.get(name)
if wrp == 'ignore': # ignore (filter) dynamic parameters
return
elif wrp == 'str2seconds':
value = MyTime.str2seconds(value)
# parameters changed - clear properties and substitution cache:
self.__properties = None
@ -340,6 +368,8 @@ class CommandAction(ActionBase):
# set:
self.__dict__[name] = value
__setitem__ = __setattr__
def __delattr__(self, name):
if not name.startswith('_'):
# parameters changed - clear properties and substitution cache:
@ -363,8 +393,8 @@ class CommandAction(ActionBase):
self.__properties = dict(
(key, getattr(self, key))
for key in dir(self)
if not key.startswith("_") and not callable(getattr(self, key)))
#
if not key.startswith("_") and not callable(getattr(self, key))
)
return self.__properties
@property
@ -372,10 +402,40 @@ class CommandAction(ActionBase):
return self.__substCache
def _getOperation(self, tag, family):
# replace operation tag (interpolate all values), be sure family is enclosed as conditional value
# (as lambda in addrepl so only if not overwritten in action):
return self.replaceTag(tag, self._properties,
conditional=('family=' + family), cache=self.__substCache)
conditional=('family='+family if family else ''),
addrepl=(lambda tag:family if tag == 'family' else None),
cache=self.__substCache)
def _executeOperation(self, tag, operation, family=[]):
def _operationExecuted(self, tag, family, *args):
""" Get, set or delete command of operation considering family.
"""
key = ('__eOpCmd',tag)
if not len(args): # get
if not callable(family): # pragma: no cover
return self.__substCache.get(key, {}).get(family)
# family as expression - use it to filter values:
return [v for f, v in self.__substCache.get(key, {}).iteritems() if family(f)]
cmd = args[0]
if cmd: # set:
try:
famd = self.__substCache[key]
except KeyError:
famd = self.__substCache[key] = {}
famd[family] = cmd
else: # delete (given family and all other with same command):
try:
famd = self.__substCache[key]
cmd = famd.pop(family)
for family, v in famd.items():
if v == cmd:
del famd[family]
except KeyError: # pragma: no cover
pass
def _executeOperation(self, tag, operation, family=[], afterExec=None):
"""Executes the operation commands (like "actionstart", "actionstop", etc).
Replace the tags in the action command with actions properties
@ -383,54 +443,95 @@ class CommandAction(ActionBase):
"""
# check valid tags in properties (raises ValueError if self recursion, etc.):
res = True
try:
# common (resp. ipv4):
startCmd = None
if not family or 'inet4' in family:
startCmd = self._getOperation(tag, 'inet4')
if startCmd:
res &= self.executeCmd(startCmd, self.timeout)
# start ipv6 actions if available:
if allowed_ipv6 and (not family or 'inet6' in family):
startCmd6 = self._getOperation(tag, 'inet6')
if startCmd6 and startCmd6 != startCmd:
res &= self.executeCmd(startCmd6, self.timeout)
if not res:
raise RuntimeError("Error %s action %s/%s" % (operation, self._jail, self._name,))
except ValueError as e:
raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, e))
err = 'Script error'
if not family: # all started:
family = [famoper for (famoper,v) in self.__started.iteritems() if v]
for famoper in family:
try:
cmd = self._getOperation(tag, famoper)
ret = True
# avoid double execution of same command for both families:
if cmd and cmd not in self._operationExecuted(tag, lambda f: f != famoper):
ret = self.executeCmd(cmd, self.timeout)
res &= ret
if afterExec: afterExec(famoper, ret)
self._operationExecuted(tag, famoper, cmd if ret else None)
except ValueError as e:
res = False
err = e
if not res:
raise RuntimeError("Error %s action %s/%s: %r" % (operation, self._jail, self._name, err))
return res
COND_FAMILIES = ('inet4', 'inet6')
@property
def _hasCondSection(self):
v = self._properties.get('__hasCondSection')
if v is not None:
return v
v = False
for n in self._properties:
if CONDITIONAL_FAM_RE.match(n):
v = True
break
self._properties['__hasCondSection'] = v
return v
@property
def _families(self):
v = self._properties.get('__families')
if v: return v
v = self._properties.get('families')
if v and not isinstance(v, (list,set)): # pragma: no cover - still unused
v = splitwords(v)
elif self._hasCondSection: # all conditional families:
# todo: check it is needed at all # common (resp. ipv4) + ipv6 if allowed:
v = ['inet4', 'inet6'] if allowed_ipv6() else ['inet4']
else: # all action tags seems to be the same
v = ['']
self._properties['__families'] = v
return v
@property
def _startOnDemand(self):
"""Checks the action depends on family (conditional)"""
v = self._properties.get('actionstart_on_demand')
if v is None:
v = False
for n in self._properties:
if CONDITIONAL_FAM_RE.match(n):
v = True
break
if v is not None:
return v
# not set - auto-recognize (depending on conditional):
v = self._hasCondSection
self._properties['actionstart_on_demand'] = v
return v
def start(self, family=[]):
def start(self):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
if not family:
# check the action depends on family (conditional):
if self._startOnDemand:
return True
elif self.__started.get(family): # pragma: no cover - normally unreachable
return True
return self._executeOperation('<actionstart>', 'starting', family=family)
return self._start()
def ban(self, aInfo):
"""Executes the "actionban" command.
def _start(self, family=None, forceStart=False):
"""Executes the "actionstart" command.
Replace the tags in the action command with actions properties
and executes the resulting command.
"""
# check the action depends on family (conditional):
if self._startOnDemand:
if not forceStart:
return True
elif not forceStart and self.__started.get(family): # pragma: no cover - normally unreachable
return True
family = [family] if family is not None else self._families
def _started(family, ret):
if ret:
self._operationExecuted('<actionstop>', family, None)
self.__started[family] = 1
ret = self._executeOperation('<actionstart>', 'starting', family=family, afterExec=_started)
return ret
def ban(self, aInfo, cmd='<actionban>'):
"""Executes the given command ("actionban" or "actionreban").
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
@ -442,21 +543,34 @@ class CommandAction(ActionBase):
the ban.
"""
# if we should start the action on demand (conditional by family):
family = aInfo.get('family', '')
if self._startOnDemand:
family = aInfo.get('family')
if not self.__started.get(family):
self.start(family)
self.__started[family] = 1
# mark also another families as "started" (-1), if they are equal
# (on demand, but the same for ipv4 and ipv6):
cmd = self._getOperation('<actionstart>', family)
for f in CommandAction.COND_FAMILIES:
if f != family and not self.__started.get(f):
if cmd == self._getOperation('<actionstart>', f):
self.__started[f] = -1
self._start(family, forceStart=True)
# ban:
if not self._processCmd('<actionban>', aInfo):
if not self._processCmd(cmd, aInfo):
raise RuntimeError("Error banning %(ip)s" % aInfo)
self.__started[family] = self.__started.get(family, 0) | 3; # started and contains items
@property
def _prolongable(self):
return (hasattr(self, 'actionprolong') and self.actionprolong
and not str(self.actionprolong).isspace())
def prolong(self, aInfo):
"""Executes the "actionprolong" command.
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
if not self._processCmd('<actionprolong>', aInfo):
raise RuntimeError("Error prolonging %(ip)s" % aInfo)
def unban(self, aInfo):
"""Executes the "actionunban" command.
@ -470,8 +584,25 @@ class CommandAction(ActionBase):
Dictionary which includes information in relation to
the ban.
"""
if not self._processCmd('<actionunban>', aInfo):
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
family = aInfo.get('family', '')
if self.__started.get(family, 0) & 2: # contains items
if not self._processCmd('<actionunban>', aInfo):
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
def reban(self, aInfo):
"""Executes the "actionreban" command if available, otherwise simply repeat "actionban".
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
# re-ban:
return self.ban(aInfo, '<actionreban>' if self.actionreban else '<actionban>')
def flush(self):
"""Executes the "actionflush" command.
@ -482,15 +613,15 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
family = []
# collect started families, if started on demand (conditional):
if self._startOnDemand:
for f in CommandAction.COND_FAMILIES:
if self.__started.get(f) == 1: # only real started:
family.append(f)
# if no started (on demand) actions:
if not family: return True
return self._executeOperation('<actionflush>', 'flushing', family=family)
# collect started families, may be started on demand (conditional):
family = [f for (f,v) in self.__started.iteritems() if v & 3 == 3]; # started and contains items
# if nothing contains items:
if not family: return True
# flush:
def _afterFlush(family, ret):
if ret and self.__started.get(family):
self.__started[family] &= ~2; # no items anymore
return self._executeOperation('<actionflush>', 'flushing', family=family, afterExec=_afterFlush)
def stop(self):
"""Executes the "actionstop" command.
@ -498,16 +629,30 @@ class CommandAction(ActionBase):
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
family = []
return self._stop()
def _stop(self, family=None):
"""Executes the "actionstop" command.
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
# collect started families, if started on demand (conditional):
if self._startOnDemand:
for f in CommandAction.COND_FAMILIES:
if self.__started.get(f) == 1: # only real started:
family.append(f)
self.__started[f] = 0
if family is None:
family = [f for (f,v) in self.__started.iteritems() if v]
# if no started (on demand) actions:
if not family: return True
return self._executeOperation('<actionstop>', 'stopping', family=family)
self.__started = {}
else:
try:
self.__started[family] &= 0
family = [family]
except KeyError: # pragma: no cover
return True
def _stopped(family, ret):
if ret:
self._operationExecuted('<actionstart>', family, None)
return self._executeOperation('<actionstop>', 'stopping', family=family, afterExec=_stopped)
def reload(self, **kwargs):
"""Executes the "actionreload" command.
@ -522,8 +667,24 @@ class CommandAction(ActionBase):
"""
return self._executeOperation('<actionreload>', 'reloading')
@staticmethod
def escapeTag(value):
def consistencyCheck(self, beforeRepair=None):
"""Executes the invariant check with repair if expected (conditional).
"""
ret = True
# for each started family:
if self.actioncheck:
for (family, started) in self.__started.items():
if started and not self._invariantCheck(family, beforeRepair):
# reset started flag and command of executed operation:
self.__started[family] = 0
self._operationExecuted('<actionstart>', family, None)
ret &= False
return ret
ESCAPE_CRE = re.compile(r"""[\\#&;`|*?~<>^()\[\]{}$'"\n\r]""")
@classmethod
def escapeTag(cls, value):
"""Escape characters which may be used for command injection.
Parameters
@ -540,16 +701,19 @@ class CommandAction(ActionBase):
-----
The following characters are escaped::
\\#&;`|*?~<>^()[]{}$'"
\\#&;`|*?~<>^()[]{}$'"\n\r
"""
for c in '\\#&;`|*?~<>^()[]{}$\'"':
if c in value:
value = value.replace(c, '\\' + c)
_map2c = {'\n': 'n', '\r': 'r'}
def substChar(m):
c = m.group()
return '\\' + _map2c.get(c, c)
value = cls.ESCAPE_CRE.sub(substChar, value)
return value
@classmethod
def replaceTag(cls, query, aInfo, conditional='', cache=None):
def replaceTag(cls, query, aInfo, conditional='', addrepl=None, cache=None):
"""Replaces tags in `query` with property values.
Parameters
@ -590,7 +754,8 @@ class CommandAction(ActionBase):
pass
# interpolation of dictionary:
if subInfo is None:
subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags)
subInfo = substituteRecursiveTags(aInfo, conditional, ignore=cls._escapedTags,
addrepl=addrepl)
# cache if possible:
if csubkey is not None:
cache[csubkey] = subInfo
@ -713,7 +878,58 @@ class CommandAction(ActionBase):
realCmd = Utils.buildShellCmd(realCmd, varsDict)
return realCmd
def _processCmd(self, cmd, aInfo=None, conditional=''):
@property
def banEpoch(self):
return getattr(self, '_banEpoch', 0)
def invalidateBanEpoch(self):
"""Increments ban epoch of jail and this action, so already banned tickets would cause
a re-ban for all tickets with previous epoch."""
if self._jail is not None:
self._banEpoch = self._jail.actions.banEpoch = self._jail.actions.banEpoch + 1
else:
self._banEpoch = self.banEpoch + 1
def _invariantCheck(self, family=None, beforeRepair=None, forceStart=True):
"""Executes a substituted `actioncheck` command.
"""
# for started action/family only (avoid check not started inet4 if inet6 gets broken):
if not forceStart and family is not None and family not in self.__started:
return 1
checkCmd = self._getOperation('<actioncheck>', family)
if not checkCmd or self.executeCmd(checkCmd, self.timeout):
return 1
# if don't need repair/restore - just return:
if beforeRepair and not beforeRepair():
return -1
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
# increment ban epoch of jail and this action (allows re-ban on already banned):
self.invalidateBanEpoch()
# try to find repair command, if exists - exec it:
repairCmd = self._getOperation('<actionrepair>', family)
if repairCmd:
if not self.executeCmd(repairCmd, self.timeout):
self.__started[family] = 0
self._logSys.critical("Unable to restore environment")
return 0
self.__started[family] = 1
else:
# no repair command, try to restart action...
# [WARNING] TODO: be sure all banactions get a repair command, because
# otherwise stop/start will theoretically remove all the bans,
# but the tickets are still in BanManager, so in case of new failures
# it will not be banned, because "already banned" will happen.
try:
self._stop(family)
except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
pass
self._start(family, forceStart=forceStart or not self._startOnDemand)
if self.__started.get(family) and not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return 0
return 1
def _processCmd(self, cmd, aInfo=None):
"""Executes a command with preliminary checks and substitutions.
Before executing any commands, executes the "check" command first
@ -738,47 +954,28 @@ class CommandAction(ActionBase):
return True
# conditional corresponding family of the given ip:
if conditional == '':
conditional = 'family=inet4'
if allowed_ipv6:
try:
ip = aInfo["ip"]
if ip and asip(ip).isIPv6:
conditional = 'family=inet6'
except KeyError:
pass
try:
family = aInfo["family"]
except (KeyError, TypeError):
family = ''
checkCmd = self.replaceTag('<actioncheck>', self._properties,
conditional=conditional, cache=self.__substCache)
if checkCmd:
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.error(
"Invariant check failed. Trying to restore a sane environment")
# try to find repair command, if exists - exec it:
repairCmd = self.replaceTag('<actionrepair>', self._properties,
conditional=conditional, cache=self.__substCache)
if repairCmd:
if not self.executeCmd(repairCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
return False
else:
# no repair command, try to restart action...
# [WARNING] TODO: be sure all banactions get a repair command, because
# otherwise stop/start will theoretically remove all the bans,
# but the tickets are still in BanManager, so in case of new failures
# it will not be banned, because "already banned" will happen.
try:
self.stop()
except RuntimeError: # bypass error in stop (if start/check succeeded hereafter).
pass
self.start()
if not self.executeCmd(checkCmd, self.timeout):
self._logSys.critical("Unable to restore environment")
# invariant check:
if self.actioncheck:
# don't repair/restore if unban (no matter):
def _beforeRepair():
if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
self._logSys.error("Invariant check failed. Unban is impossible.")
return False
return True
# check and repair if broken:
ret = self._invariantCheck(family, _beforeRepair, forceStart=(cmd != '<actionunban>'))
# if not sane (and not restored) return:
if ret != 1:
return False
# Replace static fields
realCmd = self.replaceTag(cmd, self._properties,
conditional=conditional, cache=self.__substCache)
conditional=('family='+family if family else ''), cache=self.__substCache)
# Replace dynamical tags, important - don't cache, no recursion and auto-escape here
if aInfo is not None:
@ -811,7 +1008,8 @@ class CommandAction(ActionBase):
RuntimeError
If command execution times out.
"""
logSys.debug(realCmd)
if logSys.getEffectiveLevel() < logging.DEBUG:
logSys.log(9, realCmd)
if not realCmd:
logSys.debug("Nothing to do")
return True

View file

@ -34,10 +34,12 @@ try:
except ImportError:
OrderedDict = dict
from .banmanager import BanManager
from .banmanager import BanManager, BanTicket
from .ipdns import IPAddr
from .jailthread import JailThread
from .action import ActionBase, CommandAction, CallingMap
from .mytime import MyTime
from .observer import Observers
from .utils import Utils
from ..helpers import getLogger
@ -74,12 +76,18 @@ class Actions(JailThread, Mapping):
"""
def __init__(self, jail):
JailThread.__init__(self)
JailThread.__init__(self, name="f2b/a."+jail.name)
## The jail which contains this action.
self._jail = jail
self._actions = OrderedDict()
## The ban manager.
self.__banManager = BanManager()
self.banEpoch = 0
self.__lastConsistencyCheckTM = 0
## Precedence of ban (over unban), so max number of tickets banned (to call an unban check):
self.banPrecedence = 10
## Max count of outdated tickets to unban per each __checkUnBan operation:
self.unbanMaxCount = self.banPrecedence * 2
@staticmethod
def _load_python_module(pythonModule):
@ -155,8 +163,8 @@ class Actions(JailThread, Mapping):
delacts = OrderedDict((name, action) for name, action in self._actions.iteritems()
if name not in self._reload_actions)
if len(delacts):
# unban all tickets using remove action only:
self.__flushBan(db=False, actions=delacts)
# unban all tickets using removed actions only:
self.__flushBan(db=False, actions=delacts, stop=True)
# stop and remove it:
self.stopActions(actions=delacts)
delattr(self, '_reload_actions')
@ -203,6 +211,29 @@ class Actions(JailThread, Mapping):
def getBanTime(self):
return self.__banManager.getBanTime()
def getBanList(self, withTime=False):
"""Returns the list of banned IP addresses.
Returns
-------
list
The list of banned IP addresses.
"""
return self.__banManager.getBanList(ordered=True, withTime=withTime)
def addBannedIP(self, ip):
"""Ban an IP or list of IPs."""
unixTime = MyTime.time()
if isinstance(ip, list):
# Multiple IPs:
tickets = (BanTicket(ip, unixTime) for ip in ip)
else:
# Single IP:
tickets = (BanTicket(ip, unixTime),)
return self.__checkBan(tickets)
def removeBannedIP(self, ip=None, db=True, ifexists=False):
"""Removes banned IP calling actions' unban method
@ -211,8 +242,8 @@ class Actions(JailThread, Mapping):
Parameters
----------
ip : str or IPAddr or None
The IP address to unban or all IPs if None
ip : list, str, IPAddr or None
The IP address (or multiple IPs as list) to unban or all IPs if None
Raises
------
@ -222,6 +253,19 @@ class Actions(JailThread, Mapping):
# Unban all?
if ip is None:
return self.__flushBan(db)
# Multiple IPs:
if isinstance(ip, list):
missed = []
cnt = 0
for i in ip:
try:
cnt += self.removeBannedIP(i, db, ifexists)
except ValueError:
if not ifexists:
missed.append(i)
if missed:
raise ValueError("not banned: %r" % missed)
return cnt
# Single IP:
# Always delete ip from database (also if currently not banned)
if db and self._jail.database is not None:
@ -232,9 +276,11 @@ class Actions(JailThread, Mapping):
# Unban the IP.
self.__unBan(ticket)
else:
msg = "%s is not banned" % ip
logSys.log(logging.MSG, msg)
if ifexists:
return 0
raise ValueError("%s is not banned" % ip)
raise ValueError(msg)
return 1
@ -267,6 +313,7 @@ class Actions(JailThread, Mapping):
bool
True when the thread exits nicely.
"""
cnt = 0
for name, action in self._actions.iteritems():
try:
action.start()
@ -281,10 +328,18 @@ class Actions(JailThread, Mapping):
lambda: False, self.sleeptime)
logSys.debug("Actions: leave idle mode")
continue
if not Utils.wait_for(lambda: not self.active or self.__checkBan(), self.sleeptime):
self.__checkUnBan()
# wait for ban (stop if gets inactive):
bancnt = Utils.wait_for(lambda: not self.active or self.__checkBan(), self.sleeptime)
cnt += bancnt
# unban if nothing is banned not later than banned tickets >= banPrecedence
if not bancnt or cnt >= self.banPrecedence:
if self.active:
# let shrink the ban list faster
bancnt *= 2
self.__checkUnBan(bancnt if bancnt and bancnt < self.unbanMaxCount else self.unbanMaxCount)
cnt = 0
self.__flushBan()
self.__flushBan(stop=True)
self.stopActions()
return True
@ -300,6 +355,8 @@ class Actions(JailThread, Mapping):
"fid": lambda self: self.__ticket.getID(),
"failures": lambda self: self.__ticket.getAttempt(),
"time": lambda self: self.__ticket.getTime(),
"bantime": lambda self: self._getBanTime(),
"bancount": lambda self: self.__ticket.getBanCount(),
"matches": lambda self: "\n".join(self.__ticket.getMatches()),
# to bypass actions, that should not be executed for restored tickets
"restored": lambda self: (1 if self.__ticket.restored else 0),
@ -326,6 +383,11 @@ class Actions(JailThread, Mapping):
def copy(self): # pragma: no cover
return self.__class__(self.__ticket, self.__jail, self.immutable, self.data.copy())
def _getBanTime(self):
btime = self.__ticket.getBanTime()
if btime is None: btime = self.__jail.actions.getBanTime()
return int(btime)
def _mi4ip(self, overalljails=False):
"""Gets bans merged once, a helper for lambda(s), prevents stop of executing action by any exception inside.
@ -373,11 +435,20 @@ class Actions(JailThread, Mapping):
aInfo = Actions.ActionInfo(ticket, self._jail)
return aInfo
def __getFailTickets(self, count=100):
"""Generator to get maximal count failure tickets from fail-manager."""
cnt = 0
while cnt < count:
ticket = self._jail.getFailTicket()
if not ticket:
break
yield ticket
cnt += 1
def __checkBan(self):
def __checkBan(self, tickets=None):
"""Check for IP address to ban.
Look in the jail queue for FailTicket. If a ticket is available,
If tickets are not specified look in the jail queue for FailTicket. If a ticket is available,
it executes the "ban" command and adds a ticket to the BanManager.
Returns
@ -386,17 +457,23 @@ class Actions(JailThread, Mapping):
True if an IP address get banned.
"""
cnt = 0
while cnt < 100:
ticket = self._jail.getFailTicket()
if not ticket:
break
bTicket = BanManager.createBanTicket(ticket)
if not tickets:
tickets = self.__getFailTickets(self.banPrecedence)
rebanacts = None
for ticket in tickets:
bTicket = BanTicket.wrap(ticket)
btime = ticket.getBanTime(self.__banManager.getBanTime())
ip = bTicket.getIP()
aInfo = self.__getActionInfo(bTicket)
reason = {}
if self.__banManager.addBanTicket(bTicket, reason=reason):
cnt += 1
# report ticket to observer, to check time should be increased and hereafter observer writes ban to database (asynchronous)
if Observers.Main is not None and not bTicket.restored:
Observers.Main.add('banFound', bTicket, self._jail, btime)
logSys.notice("[%s] %sBan %s", self._jail.name, ('' if not bTicket.restored else 'Restore '), ip)
# do actions :
for name, action in self._actions.iteritems():
try:
if ticket.restored and getattr(action, 'norestored', False):
@ -411,8 +488,13 @@ class Actions(JailThread, Mapping):
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# after all actions are processed set banned flag:
bTicket.banned = True
if self.banEpoch: # be sure tickets always have the same ban epoch (default 0):
bTicket.banEpoch = self.banEpoch
else:
bTicket = reason['ticket']
if reason.get('expired', 0):
logSys.info('[%s] Ignore %s, expired bantime', self._jail.name, ip)
continue
bTicket = reason.get('ticket', bTicket)
# if already banned (otherwise still process some action)
if bTicket.banned:
# compare time of failure occurrence with time ticket was really banned:
@ -425,17 +507,89 @@ class Actions(JailThread, Mapping):
else logging.NOTICE if diftm < 60 \
else logging.WARNING
logSys.log(ll, "[%s] %s already banned", self._jail.name, ip)
# if long time after ban - do consistency check (something is wrong here):
if bTicket.banEpoch == self.banEpoch and diftm > 3:
# avoid too often checks:
if not rebanacts and MyTime.time() > self.__lastConsistencyCheckTM + 3:
for action in self._actions.itervalues():
action.consistencyCheck()
self.__lastConsistencyCheckTM = MyTime.time()
# check epoch in order to reban it:
if bTicket.banEpoch < self.banEpoch:
if not rebanacts: rebanacts = dict(
(name, action) for name, action in self._actions.iteritems()
if action.banEpoch > bTicket.banEpoch)
cnt += self.__reBan(bTicket, actions=rebanacts)
else: # pragma: no cover - unexpected: ticket is not banned for some reasons - reban using all actions:
cnt += self.__reBan(bTicket)
if cnt:
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name)
return cnt
def __checkUnBan(self):
def __reBan(self, ticket, actions=None, log=True):
"""Repeat bans for the ticket.
Executes the actions in order to reban the host given in the
ticket.
Parameters
----------
ticket : Ticket
Ticket to reban
"""
actions = actions or self._actions
ip = ticket.getIP()
aInfo = self.__getActionInfo(ticket)
if log:
logSys.notice("[%s] Reban %s%s", self._jail.name, aInfo["ip"], (', action %r' % actions.keys()[0] if len(actions) == 1 else ''))
for name, action in actions.iteritems():
try:
logSys.debug("[%s] action %r: reban %s", self._jail.name, name, ip)
if not aInfo.immutable: aInfo.reset()
action.reban(aInfo)
except Exception as e:
logSys.error(
"Failed to execute reban jail '%s' action '%s' "
"info '%r': %s",
self._jail.name, name, aInfo, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
return 0
# after all actions are processed set banned flag:
ticket.banned = True
if self.banEpoch: # be sure tickets always have the same ban epoch (default 0):
ticket.banEpoch = self.banEpoch
return 1
def _prolongBan(self, ticket):
# prevent to prolong ticket that was removed in-between,
# if it in ban list - ban time already prolonged (and it stays there):
if not self.__banManager._inBanList(ticket): return
# do actions :
aInfo = None
for name, action in self._actions.iteritems():
try:
if ticket.restored and getattr(action, 'norestored', False):
continue
if not action._prolongable:
continue
if aInfo is None:
aInfo = self.__getActionInfo(ticket)
if not aInfo.immutable: aInfo.reset()
action.prolong(aInfo)
except Exception as e:
logSys.error(
"Failed to execute ban jail '%s' action '%s' "
"info '%r': %s",
self._jail.name, name, aInfo, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def __checkUnBan(self, maxCount=None):
"""Check for IP address to unban.
Unban IP addresses which are outdated.
"""
lst = self.__banManager.unBanList(MyTime.time())
lst = self.__banManager.unBanList(MyTime.time(), maxCount)
for ticket in lst:
self.__unBan(ticket)
cnt = len(lst)
@ -444,7 +598,7 @@ class Actions(JailThread, Mapping):
cnt, self.__banManager.size(), self._jail.name)
return cnt
def __flushBan(self, db=False, actions=None):
def __flushBan(self, db=False, actions=None, stop=False):
"""Flush the ban list.
Unban all IP address which are still in the banning list.
@ -463,17 +617,33 @@ class Actions(JailThread, Mapping):
# first we'll execute flush for actions supporting this operation:
unbactions = {}
for name, action in (actions if actions is not None else self._actions).iteritems():
if hasattr(action, 'flush') and action.actionflush:
logSys.notice("[%s] Flush ticket(s) with %s", self._jail.name, name)
action.flush()
else:
unbactions[name] = action
try:
if hasattr(action, 'flush') and (not isinstance(action, CommandAction) or action.actionflush):
logSys.notice("[%s] Flush ticket(s) with %s", self._jail.name, name)
if action.flush():
continue
except Exception as e:
logSys.error("Failed to flush bans in jail '%s' action '%s': %s",
self._jail.name, name, e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.info("No flush occurred, do consistency check")
if hasattr(action, 'consistencyCheck'):
def _beforeRepair():
if stop and not getattr(action, 'actionrepair_on_unban', None): # don't need repair on stop
self._logSys.error("Invariant check failed. Flush is impossible.")
return False
return True
action.consistencyCheck(_beforeRepair)
continue
# fallback to single unbans:
logSys.debug(" Unban tickets each individualy")
unbactions[name] = action
actions = unbactions
# flush the database also:
if db and self._jail.database is not None:
logSys.debug(" Flush jail in database")
self._jail.database.delBan(self._jail)
# unban each ticket with non-flasheable actions:
# unban each ticket with non-flusheable actions:
for ticket in lst:
# unban ip:
self.__unBan(ticket, actions=actions, log=log)
@ -503,8 +673,6 @@ class Actions(JailThread, Mapping):
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"])
for name, action in unbactions.iteritems():
try:
if ticket.restored and getattr(action, 'norestored', False):
continue
logSys.debug("[%s] action %r: unban %s", self._jail.name, name, ip)
if not aInfo.immutable: aInfo.reset()
action.unban(aInfo)

View file

@ -102,9 +102,22 @@ class BanManager:
#
# @return IP list
def getBanList(self):
def getBanList(self, ordered=False, withTime=False):
with self.__lock:
return self.__banList.keys()
if not ordered:
return self.__banList.keys()
lst = []
for ticket in self.__banList.itervalues():
eob = ticket.getEndOfBanTime(self.__banTime)
lst.append((ticket,eob))
lst.sort(key=lambda t: t[1])
t2s = MyTime.time2str
if withTime:
return ['%s \t%s + %d = %s' % (
t[0].getID(),
t2s(t[0].getTime()), t[0].getBanTime(self.__banTime), t2s(t[1])
) for t in lst]
return [t[0].getID() for t in lst]
##
# Returns a iterator to ban list (used in reload, so idle).
@ -249,21 +262,6 @@ class BanManager:
logSys.exception(e)
return []
##
# Create a ban ticket.
#
# Create a BanTicket from a FailTicket. The timestamp of the BanTicket
# is the current time. This is a static method.
# @param ticket the FailTicket
# @return a BanTicket
@staticmethod
def createBanTicket(ticket):
# we should always use correct time to calculate correct end time (ban time is variable now,
# + possible double banning by restore from database and from log file)
# so use as lastTime always time from ticket.
return BanTicket(ticket=ticket)
##
# Add a ban ticket.
#
@ -273,6 +271,9 @@ class BanManager:
def addBanTicket(self, ticket, reason={}):
eob = ticket.getEndOfBanTime(self.__banTime)
if eob < MyTime.time():
reason['expired'] = 1
return False
with self.__lock:
# check already banned
fid = ticket.getID()
@ -294,6 +295,7 @@ class BanManager:
# not yet banned - add new one:
self.__banList[fid] = ticket
self.__banTotal += 1
ticket.incrBanCount()
# correct next unban time:
if self.__nextUnbanTime > eob:
self.__nextUnbanTime = eob
@ -325,27 +327,32 @@ class BanManager:
# @param time the time
# @return the list of ticket to unban
def unBanList(self, time):
def unBanList(self, time, maxCount=0x7fffffff):
with self.__lock:
# Permanent banning
if self.__banTime < 0:
return list()
# Check next unban time:
if self.__nextUnbanTime > time:
nextUnbanTime = self.__nextUnbanTime
if nextUnbanTime > time:
return list()
# Gets the list of ticket to remove (thereby correct next unban time).
unBanList = {}
self.__nextUnbanTime = BanTicket.MAX_TIME
nextUnbanTime = BanTicket.MAX_TIME
for fid,ticket in self.__banList.iteritems():
# current time greater as end of ban - timed out:
eob = ticket.getEndOfBanTime(self.__banTime)
if time > eob:
unBanList[fid] = ticket
elif self.__nextUnbanTime > eob:
self.__nextUnbanTime = eob
if len(unBanList) >= maxCount: # stop search cycle, so reset back the next check time
nextUnbanTime = self.__nextUnbanTime
break
elif nextUnbanTime > eob:
nextUnbanTime = eob
self.__nextUnbanTime = nextUnbanTime
# Removes tickets.
if len(unBanList):
if len(unBanList) / 2.0 <= len(self.__banList) / 3.0:

View file

@ -138,7 +138,7 @@ class Fail2BanDb(object):
filename
purgeage
"""
__version__ = 2
__version__ = 4
# Note all SCRIPTS strings must end in ';' for py26 compatibility
_CREATE_SCRIPTS = (
('fail2banDb', "CREATE TABLE IF NOT EXISTS fail2banDb(version INTEGER);")
@ -166,21 +166,36 @@ class Fail2BanDb(object):
"jail TEXT NOT NULL, " \
"ip TEXT, " \
"timeofban INTEGER NOT NULL, " \
"bantime INTEGER NOT NULL, " \
"bancount INTEGER NOT NULL default 1, " \
"data JSON, " \
"FOREIGN KEY(jail) REFERENCES jails(name) " \
");" \
"CREATE INDEX IF NOT EXISTS bans_jail_timeofban_ip ON bans(jail, timeofban);" \
"CREATE INDEX IF NOT EXISTS bans_jail_ip ON bans(jail, ip);" \
"CREATE INDEX IF NOT EXISTS bans_ip ON bans(ip);")
,('bips', "CREATE TABLE IF NOT EXISTS bips(" \
"ip TEXT NOT NULL, " \
"jail TEXT NOT NULL, " \
"timeofban INTEGER NOT NULL, " \
"bantime INTEGER NOT NULL, " \
"bancount INTEGER NOT NULL default 1, " \
"data JSON, " \
"PRIMARY KEY(ip, jail), " \
"FOREIGN KEY(jail) REFERENCES jails(name) " \
");" \
"CREATE INDEX IF NOT EXISTS bips_timeofban ON bips(timeofban);" \
"CREATE INDEX IF NOT EXISTS bips_ip ON bips(ip);")
)
_CREATE_TABS = dict(_CREATE_SCRIPTS)
def __init__(self, filename, purgeAge=24*60*60):
self.maxEntries = 50
def __init__(self, filename, purgeAge=24*60*60, outDatedFactor=3):
self.maxMatches = 10
self._lock = RLock()
self._dbFilename = filename
self._purgeAge = purgeAge
self._outDatedFactor = outDatedFactor;
self._connectDB()
def _connectDB(self, checkIntegrity=False):
@ -366,13 +381,32 @@ class Fail2BanDb(object):
if version < 2 and self._tableExists(cur, "logs"):
cur.executescript("BEGIN TRANSACTION;"
"CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;"
"DROP TABLE logs;"
"%s;"
"INSERT INTO logs SELECT * from logs_temp;"
"DROP TABLE logs_temp;"
"UPDATE fail2banDb SET version = 2;"
"COMMIT;" % Fail2BanDb._CREATE_TABS['logs'])
"CREATE TEMPORARY TABLE logs_temp AS SELECT * FROM logs;"
"DROP TABLE logs;"
"%s;"
"INSERT INTO logs SELECT * from logs_temp;"
"DROP TABLE logs_temp;"
"UPDATE fail2banDb SET version = 2;"
"COMMIT;" % Fail2BanDb._CREATE_TABS['logs'])
if version < 3 and self._tableExists(cur, "bans"):
# set ban-time to -2 (note it means rather unknown, as persistent, will be fixed by restore):
cur.executescript("BEGIN TRANSACTION;"
"CREATE TEMPORARY TABLE bans_temp AS SELECT jail, ip, timeofban, -2 as bantime, 1 as bancount, data FROM bans;"
"DROP TABLE bans;"
"%s;\n"
"INSERT INTO bans SELECT * from bans_temp;"
"DROP TABLE bans_temp;"
"COMMIT;" % Fail2BanDb._CREATE_TABS['bans'])
if version < 4 and not self._tableExists(cur, "bips"):
cur.executescript("BEGIN TRANSACTION;"
"%s;\n"
"UPDATE fail2banDb SET version = 4;"
"COMMIT;" % Fail2BanDb._CREATE_TABS['bips'])
if self._tableExists(cur, "bans"):
cur.execute(
"INSERT OR REPLACE INTO bips(ip, jail, timeofban, bantime, bancount, data)"
" SELECT ip, jail, timeofban, bantime, bancount, data FROM bans order by timeofban")
cur.execute("SELECT version FROM fail2banDb LIMIT 1")
return cur.fetchone()[0]
@ -541,11 +575,21 @@ class Fail2BanDb(object):
#TODO: Implement data parts once arbitrary match keys completed
data = ticket.getData()
matches = data.get('matches')
if matches and len(matches) > self.maxEntries:
data['matches'] = matches[-self.maxEntries:]
if self.maxMatches:
if matches and len(matches) > self.maxMatches:
data = data.copy()
data['matches'] = matches[-self.maxMatches:]
elif matches:
data = data.copy()
del data['matches']
cur.execute(
"INSERT INTO bans(jail, ip, timeofban, data) VALUES(?, ?, ?, ?)",
(jail.name, ip, int(round(ticket.getTime())), data))
"INSERT INTO bans(jail, ip, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)",
(jail.name, ip, int(round(ticket.getTime())), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount(),
data))
cur.execute(
"INSERT OR REPLACE INTO bips(ip, jail, timeofban, bantime, bancount, data) VALUES(?, ?, ?, ?, ?, ?)",
(ip, jail.name, int(round(ticket.getTime())), ticket.getBanTime(jail.actions.getBanTime()), ticket.getBanCount(),
data))
@commitandrollback
def delBan(self, cur, jail, *args):
@ -558,16 +602,20 @@ class Fail2BanDb(object):
args : list of IP
IPs to be removed, if not given all tickets of jail will be removed.
"""
query = "DELETE FROM bans WHERE jail = ?"
query1 = "DELETE FROM bips WHERE jail = ?"
query2 = "DELETE FROM bans WHERE jail = ?"
queryArgs = [jail.name];
if not len(args):
cur.execute(query, queryArgs);
cur.execute(query1, queryArgs);
cur.execute(query2, queryArgs);
return
query += " AND ip = ?"
query1 += " AND ip = ?"
query2 += " AND ip = ?"
queryArgs.append('');
for ip in args:
queryArgs[1] = str(ip);
cur.execute(query, queryArgs);
cur.execute(query1, queryArgs);
cur.execute(query2, queryArgs);
@commitandrollback
def _getBans(self, cur, jail=None, bantime=None, ip=None):
@ -667,7 +715,7 @@ class Fail2BanDb(object):
tickdata = {}
m = data.get('matches', [])
# pre-insert "maxadd" enries (because tickets are ordered desc by time)
maxadd = self.maxEntries - len(matches)
maxadd = self.maxMatches - len(matches)
if maxadd > 0:
if len(m) <= maxadd:
matches = m + matches
@ -685,42 +733,134 @@ class Fail2BanDb(object):
self._bansMergedCache[cacheKey] = tickets if ip is None else ticket
return tickets if ip is None else ticket
@commitandrollback
def getBan(self, cur, ip, jail=None, forbantime=None, overalljails=None, fromtime=None):
ip = str(ip)
if not overalljails:
query = "SELECT bancount, timeofban, bantime FROM bips"
else:
query = "SELECT sum(bancount), max(timeofban), sum(bantime) FROM bips"
query += " WHERE ip = ?"
queryArgs = [ip]
if not overalljails and jail is not None:
query += " AND jail=?"
queryArgs.append(jail.name)
if forbantime is not None:
query += " AND timeofban > ?"
queryArgs.append(MyTime.time() - forbantime)
if fromtime is not None:
query += " AND timeofban > ?"
queryArgs.append(fromtime)
if overalljails or jail is None:
query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor()
return cur.execute(query, queryArgs)
def _getCurrentBans(self, cur, jail = None, ip = None, forbantime=None, fromtime=None):
if fromtime is None:
fromtime = MyTime.time()
queryArgs = []
if jail is not None:
query = "SELECT ip, timeofban, data FROM bans WHERE jail=?"
query = "SELECT ip, timeofban, bantime, bancount, data FROM bips WHERE jail=?"
queryArgs.append(jail.name)
else:
query = "SELECT ip, max(timeofban), data FROM bans WHERE 1"
query = "SELECT ip, max(timeofban), bantime, bancount, data FROM bips WHERE 1"
if ip is not None:
query += " AND ip=?"
queryArgs.append(ip)
query += " AND (timeofban + bantime > ? OR bantime <= -1)"
queryArgs.append(fromtime)
if forbantime not in (None, -1): # not specified or persistent (all)
query += " AND timeofban > ?"
queryArgs.append(fromtime - forbantime)
if ip is None:
query += " GROUP BY ip ORDER BY ip, timeofban DESC"
else:
query += " ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor()
return cur.execute(query, queryArgs)
def getCurrentBans(self, jail = None, ip = None, forbantime=None, fromtime=None):
@commitandrollback
def getCurrentBans(self, cur, jail=None, ip=None, forbantime=None, fromtime=None,
correctBanTime=True, maxmatches=None
):
"""Reads tickets (with merged info) currently affected from ban from the database.
There are all the tickets corresponding parameters jail/ip, forbantime,
fromtime (normally now).
If correctBanTime specified (default True) it will fix the restored ban-time
(and therefore endOfBan) of the ticket (normally it is ban-time of jail as maximum)
for all tickets with ban-time greater (or persistent).
"""
if fromtime is None:
fromtime = MyTime.time()
tickets = []
ticket = None
if correctBanTime is True:
correctBanTime = jail.getMaxBanTime() if jail is not None else None
# don't change if persistent allowed:
if correctBanTime == -1: correctBanTime = None
with self._lock:
results = list(self._getCurrentBans(self._db.cursor(),
jail=jail, ip=ip, forbantime=forbantime, fromtime=fromtime))
for ticket in self._getCurrentBans(cur, jail=jail, ip=ip,
forbantime=forbantime, fromtime=fromtime
):
# can produce unpack error (database may return sporadical wrong-empty row):
try:
banip, timeofban, bantime, bancount, data = ticket
# additionally check for empty values:
if banip is None or banip == "": # pragma: no cover
raise ValueError('unexpected value %r' % (banip,))
# if bantime unknown (after upgrade-db from earlier version), just use min known ban-time:
if bantime == -2: # todo: remove it in future version
bantime = jail.actions.getBanTime() if jail is not None else (
correctBanTime if correctBanTime else 600)
elif correctBanTime and correctBanTime >= 0:
# if persistent ban (or greater as max), use current max-bantime of the jail:
if bantime == -1 or bantime > correctBanTime:
bantime = correctBanTime
# after correction check the end of ban again:
if bantime != -1 and timeofban + bantime <= fromtime:
# not persistent and too old - ignore it:
logSys.debug("ignore ticket (with new max ban-time %r): too old %r <= %r, ticket: %r",
bantime, timeofban + bantime, fromtime, ticket)
continue
except ValueError as e: # pragma: no cover
logSys.debug("get current bans: ignore row %r - %s", ticket, e)
continue
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
ticket = FailTicket(banip, timeofban, data=data)
# filter matches if expected (current count > as maxmatches specified):
if maxmatches is None:
maxmatches = self.maxMatches
if maxmatches:
matches = ticket.getMatches()
if matches and len(matches) > maxmatches:
ticket.setMatches(matches[-maxmatches:])
else:
ticket.setMatches(None)
# logSys.debug('restored ticket: %r', ticket)
ticket.setBanTime(bantime)
ticket.setBanCount(bancount)
if ip is not None: return ticket
tickets.append(ticket)
if results:
for banip, timeofban, data in results:
# logSys.debug('restore ticket %r, %r, %r', banip, timeofban, data)
ticket = FailTicket(banip, timeofban, data=data)
# logSys.debug('restored ticket: %r', ticket)
tickets.append(ticket)
return tickets
return tickets if ip is None else ticket
def _cleanjails(self, cur):
"""Remove empty jails jails and log files from database.
"""
cur.execute(
"DELETE FROM jails WHERE enabled = 0 "
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name) "
"AND NOT EXISTS(SELECT * FROM bips WHERE jail = jails.name)")
def _purge_bips(self, cur):
"""Purge old bad ips (jails and log files from database).
Currently it is timed out IP, whose time since last ban is several times out-dated (outDatedFactor is default 3).
Permanent banned ips will be never removed.
"""
cur.execute(
"DELETE FROM bips WHERE timeofban < ? and bantime != -1 and (timeofban + (bantime * ?)) < ?",
(int(MyTime.time()) - self._purgeAge, self._outDatedFactor, int(MyTime.time()) - self._purgeAge))
@commitandrollback
def purge(self, cur):
@ -730,7 +870,6 @@ class Fail2BanDb(object):
cur.execute(
"DELETE FROM bans WHERE timeofban < ?",
(MyTime.time() - self._purgeAge, ))
cur.execute(
"DELETE FROM jails WHERE enabled = 0 "
"AND NOT EXISTS(SELECT * FROM bans WHERE jail = jails.name)")
self._purge_bips(cur)
self._cleanjails(cur)

View file

@ -128,52 +128,52 @@ class DateDetectorCache(object):
# 2005-01-23T21:59:59.981746, 2005-01-23 21:59:59, 2005-01-23 8:59:59
# simple date: 2005/01/23 21:59:59
# custom for syslog-ng 2006.12.21 06:43:20
"%ExY(?P<_sep>[-/.])%m(?P=_sep)%d(?:T| ?)%H:%M:%S(?:[.,]%f)?(?:\s*%z)?",
r"%ExY(?P<_sep>[-/.])%m(?P=_sep)%d(?:T| ?)%H:%M:%S(?:[.,]%f)?(?:\s*%z)?",
# asctime with optional day, subsecond and/or year:
# Sun Jan 23 21:59:59.011 2005
"(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
r"(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
# asctime with optional day, subsecond and/or year coming after day
# http://bugs.debian.org/798923
# Sun Jan 23 2005 21:59:59.011
"(?:%a )?%b %d %ExY %k:%M:%S(?:\.%f)?",
r"(?:%a )?%b %d %ExY %k:%M:%S(?:\.%f)?",
# simple date too (from x11vnc): 23/01/2005 21:59:59
# and with optional year given by 2 digits: 23/01/05 21:59:59
# (See http://bugs.debian.org/537610)
# 17-07-2008 17:23:25
"%d(?P<_sep>[-/])%m(?P=_sep)(?:%ExY|%Exy) %k:%M:%S",
r"%d(?P<_sep>[-/])%m(?P=_sep)(?:%ExY|%Exy) %k:%M:%S",
# Apache format optional time zone:
# [31/Oct/2006:09:22:55 -0000]
# 26-Jul-2007 15:20:52
# named 26-Jul-2007 15:20:52.252
# roundcube 26-Jul-2007 15:20:52 +0200
"%d(?P<_sep>[-/])%b(?P=_sep)%ExY[ :]?%H:%M:%S(?:\.%f)?(?: %z)?",
r"%d(?P<_sep>[-/])%b(?P=_sep)%ExY[ :]?%H:%M:%S(?:\.%f)?(?: %z)?",
# CPanel 05/20/2008:01:57:39
"%m/%d/%ExY:%H:%M:%S",
r"%m/%d/%ExY:%H:%M:%S",
# 01-27-2012 16:22:44.252
# subseconds explicit to avoid possible %m<->%d confusion
# with previous ("%d-%m-%ExY %k:%M:%S" by "%d(?P<_sep>[-/])%m(?P=_sep)(?:%ExY|%Exy) %k:%M:%S")
"%m-%d-%ExY %k:%M:%S(?:\.%f)?",
r"%m-%d-%ExY %k:%M:%S(?:\.%f)?",
# Epoch
"EPOCH",
r"EPOCH",
# Only time information in the log
"{^LN-BEG}%H:%M:%S",
r"{^LN-BEG}%H:%M:%S",
# <09/16/08@05:03:30>
"^<%m/%d/%Exy@%H:%M:%S>",
r"^<%m/%d/%Exy@%H:%M:%S>",
# MySQL: 130322 11:46:11
"%Exy%Exm%Exd ?%H:%M:%S",
r"%Exy%Exm%Exd ?%H:%M:%S",
# Apache Tomcat
"%b %d, %ExY %I:%M:%S %p",
r"%b %d, %ExY %I:%M:%S %p",
# ASSP: Apr-27-13 02:33:06
"^%b-%d-%Exy %k:%M:%S",
r"^%b-%d-%Exy %k:%M:%S",
# 20050123T215959, 20050123 215959, 20050123 85959
"%ExY%Exm%Exd(?:T| ?)%ExH%ExM%ExS(?:[.,]%f)?(?:\s*%z)?",
r"%ExY%Exm%Exd(?:T| ?)%ExH%ExM%ExS(?:[.,]%f)?(?:\s*%z)?",
# prefixed with optional named time zone (monit):
# PDT Apr 16 21:05:29
"(?:%Z )?(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
r"(?:%Z )?(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
# +00:00 Jan 23 21:59:59.011 2005
"(?:%z )?(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
r"(?:%z )?(?:%a )?%b %d %k:%M:%S(?:\.%f)?(?: %ExY)?",
# TAI64N
"TAI64N",
r"TAI64N",
]
@property

View file

@ -82,7 +82,7 @@ class DateTemplate(object):
return self._regex
def setRegex(self, regex, wordBegin=True, wordEnd=True):
"""Sets regex to use for searching for date in log line.
r"""Sets regex to use for searching for date in log line.
Parameters
----------
@ -301,14 +301,17 @@ class DatePatternRegex(DateTemplate):
if wordBegin and RE_EXLINE_BOUND_BEG.search(pattern):
pattern = RE_EXLINE_BOUND_BEG.sub('', pattern)
wordBegin = 'start'
# wrap to regex:
fmt = self._patternRE.sub(r'%(\1)s', pattern)
self.name = fmt % self._patternName
regex = fmt % timeRE
# if expected add (?iu) for "ignore case" and "unicode":
if RE_ALPHA_PATTERN.search(pattern):
regex = r'(?iu)' + regex
super(DatePatternRegex, self).setRegex(regex, wordBegin, wordEnd)
try:
# wrap to regex:
fmt = self._patternRE.sub(r'%(\1)s', pattern)
self.name = fmt % self._patternName
regex = fmt % timeRE
# if expected add (?iu) for "ignore case" and "unicode":
if RE_ALPHA_PATTERN.search(pattern):
regex = r'(?iu)' + regex
super(DatePatternRegex, self).setRegex(regex, wordBegin, wordEnd)
except Exception as e:
raise TypeError("Failed to set datepattern '%s' (may be an invalid format or unescaped percent char): %s" % (pattern, e))
def getDate(self, line, dateMatch=None, default_tz=None):
"""Method to return the date for a log line.

View file

@ -27,7 +27,7 @@ __license__ = "GPL"
from threading import Lock
import logging
from .ticket import FailTicket
from .ticket import FailTicket, BanTicket
from ..helpers import getLogger, BgService
# Gets the instance of the logger.
@ -43,7 +43,7 @@ class FailManager:
self.__maxRetry = 3
self.__maxTime = 600
self.__failTotal = 0
self.maxEntries = 50
self.maxMatches = 50
self.__bgSvc = BgService()
def setFailTotal(self, value):
@ -75,7 +75,7 @@ class FailManager:
def getMaxTime(self):
return self.__maxTime
def addFailure(self, ticket, count=1):
def addFailure(self, ticket, count=1, observed=False):
attempts = 1
with self.__lock:
fid = ticket.getID()
@ -87,7 +87,7 @@ class FailManager:
attempt = 1
else:
# will be incremented / extended (be sure we have at least +1 attempt):
matches = ticket.getMatches()
matches = ticket.getMatches() if self.maxMatches else None
attempt = ticket.getAttempt()
if attempt <= 0:
attempt += 1
@ -97,16 +97,22 @@ class FailManager:
fData.setLastReset(unixTime)
fData.setRetry(0)
fData.inc(matches, attempt, count)
# truncate to maxEntries:
matches = fData.getMatches()
if len(matches) > self.maxEntries:
fData.setMatches(matches[-self.maxEntries:])
# truncate to maxMatches:
if self.maxMatches:
matches = fData.getMatches()
if len(matches) > self.maxMatches:
fData.setMatches(matches[-self.maxMatches:])
else:
fData.setMatches(None)
except KeyError:
# not found - already banned - prevent to add failure if comes from observer:
if observed or isinstance(ticket, BanTicket):
return ticket.getRetry()
# if already FailTicket - add it direct, otherwise create (using copy all ticket data):
if isinstance(ticket, FailTicket):
fData = ticket;
else:
fData = FailTicket(ticket=ticket)
fData = FailTicket.wrap(ticket)
if count > ticket.getAttempt():
fData.setRetry(count)
self.__failList[fid] = fData
@ -159,7 +165,7 @@ class FailManager:
def toBan(self, fid=None):
with self.__lock:
for fid in ([fid] if fid != None and fid in self.__failList else self.__failList):
for fid in ([fid] if fid is not None and fid in self.__failList else self.__failList):
data = self.__failList[fid]
if data.getRetry() >= self.__maxRetry:
del self.__failList[fid]

View file

@ -37,25 +37,28 @@ R_HOST = [
r"""(?:::f{4,6}:)?(?P<ip4>%s)""" % (IPAddr.IP_4_RE,),
# separated ipv6:
r"""(?P<ip6>%s)""" % (IPAddr.IP_6_RE,),
# place-holder for ipv6 enclosed in optional [] (used in addr-, host-regex)
"",
# separated dns:
r"""(?P<dns>[\w\-.^_]*\w)""",
# place-holder for ADDR tag-replacement (joined):
"",
# place-holder for HOST tag replacement (joined):
""
"",
# CIDR in simplest integer form:
r"(?P<cidr>\d+)",
# place-holder for SUBNET tag-replacement
"",
]
RI_IPV4 = 0
RI_IPV6 = 1
RI_IPV6BR = 2
RI_DNS = 3
RI_ADDR = 4
RI_HOST = 5
RI_DNS = 2
RI_ADDR = 3
RI_HOST = 4
RI_CIDR = 5
RI_SUBNET = 6
R_HOST[RI_IPV6BR] = r"""\[?%s\]?""" % (R_HOST[RI_IPV6],)
R_HOST[RI_ADDR] = "(?:%s)" % ("|".join((R_HOST[RI_IPV4], R_HOST[RI_IPV6BR])),)
R_HOST[RI_HOST] = "(?:%s)" % ("|".join((R_HOST[RI_IPV4], R_HOST[RI_IPV6BR], R_HOST[RI_DNS])),)
R_HOST[RI_ADDR] = r"\[?(?:%s|%s)\]?" % (R_HOST[RI_IPV4], R_HOST[RI_IPV6],)
R_HOST[RI_HOST] = r"(?:%s|%s)" % (R_HOST[RI_ADDR], R_HOST[RI_DNS],)
R_HOST[RI_SUBNET] = r"\[?(?:%s|%s)(?:/%s)?\]?" % (R_HOST[RI_IPV4], R_HOST[RI_IPV6], R_HOST[RI_CIDR],)
RH4TAG = {
# separated ipv4 (self closed, closed):
@ -68,6 +71,11 @@ RH4TAG = {
# for separate usage of 2 address groups only (regardless of `usedns`), `ip4` and `ip6` together
"ADDR": R_HOST[RI_ADDR],
"F-ADDR/": R_HOST[RI_ADDR],
# subnet tags for usage as `<ADDR>/<CIDR>` or `<SUBNET>`:
"CIDR": R_HOST[RI_CIDR],
"F-CIDR/": R_HOST[RI_CIDR],
"SUBNET": R_HOST[RI_SUBNET],
"F-SUBNET/":R_HOST[RI_SUBNET],
# separated dns (self closed, closed):
"DNS": R_HOST[RI_DNS],
"F-DNS/": R_HOST[RI_DNS],
@ -416,3 +424,7 @@ class FailRegex(Regex):
def getHost(self):
return self.getFailID(("ip4", "ip6", "dns"))
def getIP(self):
fail = self.getGroups()
return IPAddr(self.getFailID(("ip4", "ip6")), int(fail.get("cidr") or IPAddr.CIDR_UNSPEC))

View file

@ -33,6 +33,7 @@ import time
from .actions import Actions
from .failmanager import FailManagerEmpty, FailManager
from .ipdns import DNSUtils, IPAddr
from .observer import Observers
from .ticket import FailTicket
from .jailthread import JailThread
from .datedetector import DateDetector, validateTimeZone
@ -109,6 +110,8 @@ class Filter(JailThread):
self.checkFindTime = True
## Ticks counter
self.ticks = 0
## Thread name:
self.name="f2b/f."+self.jailName
self.dateDetector = DateDetector()
logSys.debug("Created %s", self)
@ -427,23 +430,9 @@ class Filter(JailThread):
)
else:
self.__ignoreCache = None
##
# Ban an IP - http://blogs.buanzo.com.ar/2009/04/fail2ban-patch-ban-ip-address-manually.html
# Arturo 'Buanzo' Busleiman <buanzo@buanzo.com.ar>
#
# to enable banip fail2ban-client BAN command
def addBannedIP(self, ip):
if not isinstance(ip, IPAddr):
ip = IPAddr(ip)
unixTime = MyTime.time()
ticket = FailTicket(ip, unixTime)
if self._inIgnoreIPList(ip, ticket, log_ignore=False):
logSys.warning('Requested to manually ban an ignored IP %s. User knows best. Proceeding to ban it.', ip)
self.failManager.addFailure(ticket, self.failManager.getMaxRetry())
# Perform the banning of the IP now.
def performBan(self, ip=None):
"""Performs a ban for IPs (or given ip) that are reached maxretry of the jail."""
try: # pragma: no branch - exception is the only way out
while True:
ticket = self.failManager.toBan(ip)
@ -451,7 +440,24 @@ class Filter(JailThread):
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
return ip
def addAttempt(self, ip, *matches):
"""Generate a failed attempt for ip"""
if not isinstance(ip, IPAddr):
ip = IPAddr(ip)
matches = list(matches) # tuple to list
# Generate the failure attempt for the IP:
unixTime = MyTime.time()
ticket = FailTicket(ip, unixTime, matches=matches)
logSys.info(
"[%s] Attempt %s - %s", self.jailName, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S")
)
self.failManager.addFailure(ticket, len(matches) or 1)
# Perform the ban if this attempt is resulted to:
self.performBan(ip)
return 1
##
# Ignore own IP/DNS.
@ -601,9 +607,12 @@ class Filter(JailThread):
if self._inIgnoreIPList(ip, tick):
continue
logSys.info(
"[%s] Found %s - %s", self.jailName, ip, datetime.datetime.fromtimestamp(unixTime).strftime("%Y-%m-%d %H:%M:%S")
"[%s] Found %s - %s", self.jailName, ip, MyTime.time2str(unixTime)
)
self.failManager.addFailure(tick)
# report to observer - failure was found, for possibly increasing of it retry counter (asynchronous)
if Observers.Main is not None:
Observers.Main.add('failureFound', self.failManager, self.jail, tick)
# reset (halve) error counter (successfully processed line):
if self._errors:
self._errors //= 2
@ -862,12 +871,12 @@ class Filter(JailThread):
# ip-address or host:
host = fail.get('ip4')
if host is not None:
cidr = IPAddr.FAM_IPv4
cidr = int(fail.get('cidr') or IPAddr.FAM_IPv4)
raw = True
else:
host = fail.get('ip6')
if host is not None:
cidr = IPAddr.FAM_IPv6
cidr = int(fail.get('cidr') or IPAddr.FAM_IPv6)
raw = True
if host is None:
host = fail.get('dns')
@ -878,6 +887,7 @@ class Filter(JailThread):
fid = failRegex.getFailID()
host = fid
cidr = IPAddr.CIDR_RAW
raw = True
# if mlfid case (not failure):
if host is None:
if ll <= 7: logSys.log(7, "No failure-id by mlfid %r in regex %s: %s",
@ -1088,7 +1098,7 @@ class FileFilter(Filter):
fs = container.getFileSize()
if logSys.getEffectiveLevel() <= logging.DEBUG:
logSys.debug("Seek to find time %s (%s), file size %s", date,
datetime.datetime.fromtimestamp(date).strftime("%Y-%m-%d %H:%M:%S"), fs)
MyTime.time2str(date), fs)
minp = container.getPos()
maxp = fs
tryPos = minp
@ -1167,7 +1177,7 @@ class FileFilter(Filter):
container.setPos(foundPos)
if logSys.getEffectiveLevel() <= logging.DEBUG:
logSys.debug("Position %s from %s, found time %s (%s) within %s seeks", lastPos, fs, foundTime,
(datetime.datetime.fromtimestamp(foundTime).strftime("%Y-%m-%d %H:%M:%S") if foundTime is not None else ''), cntr)
(MyTime.time2str(foundTime) if foundTime is not None else ''), cntr)
def status(self, flavor="basic"):
"""Status of Filter plus files being monitored.

View file

@ -64,7 +64,7 @@ class FilterGamin(FileFilter):
logSys.debug("Created FilterGamin")
def callback(self, path, event):
logSys.debug("Got event: " + repr(event) + " for " + path)
logSys.log(4, "Got event: " + repr(event) + " for " + path)
if event in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists):
logSys.debug("File changed: " + path)
self.__modified = True
@ -79,12 +79,7 @@ class FilterGamin(FileFilter):
this is a common logic and must be shared/provided by FileFilter
"""
self.getFailures(path)
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.performBan()
self.__modified = False
##

View file

@ -98,8 +98,8 @@ class FilterPoll(FileFilter):
def run(self):
while self.active:
try:
if logSys.getEffectiveLevel() <= 6:
logSys.log(6, "Woke up idle=%s with %d files monitored",
if logSys.getEffectiveLevel() <= 4:
logSys.log(4, "Woke up idle=%s with %d files monitored",
self.idle, self.getLogCount())
if self.idle:
if not Utils.wait_for(lambda: not self.active or not self.idle,
@ -117,12 +117,7 @@ class FilterPoll(FileFilter):
self.ticks += 1
if self.__modified:
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.performBan()
self.__modified = False
except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop...
@ -145,10 +140,10 @@ class FilterPoll(FileFilter):
logStats = os.stat(filename)
stats = logStats.st_mtime, logStats.st_ino, logStats.st_size
pstats = self.__prevStats.get(filename, (0))
if logSys.getEffectiveLevel() <= 5:
if logSys.getEffectiveLevel() <= 4:
# we do not want to waste time on strftime etc if not necessary
dt = logStats.st_mtime - pstats[0]
logSys.log(5, "Checking %s for being modified. Previous/current stats: %s / %s. dt: %s",
logSys.log(4, "Checking %s for being modified. Previous/current stats: %s / %s. dt: %s",
filename, pstats, stats, dt)
# os.system("stat %s | grep Modify" % filename)
self.__file404Cnt[filename] = 0

View file

@ -87,7 +87,7 @@ class FilterPyinotify(FileFilter):
logSys.debug("Created FilterPyinotify")
def callback(self, event, origin=''):
logSys.log(7, "[%s] %sCallback for Event: %s", self.jailName, origin, event)
logSys.log(4, "[%s] %sCallback for Event: %s", self.jailName, origin, event)
path = event.pathname
# check watching of this path:
isWF = False
@ -140,12 +140,7 @@ class FilterPyinotify(FileFilter):
"""
if not self.idle:
self.getFailures(path)
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.performBan()
self.__modified = False
def _addPending(self, path, reason, isDir=False):

View file

@ -86,10 +86,14 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
files.extend(glob.glob(p))
args['files'] = list(set(files))
# Default flags is SYSTEM_ONLY(4). This would lead to ignore user session files,
# so can prevent "Too many open files" errors on a lot of user sessions (see gh-2392):
try:
args['flags'] = int(kwargs.pop('journalflags'))
except KeyError:
pass
# be sure all journal types will be opened if files specified (don't set flags):
if 'files' not in args or not len(args['files']):
args['flags'] = 4
return args
@ -307,12 +311,8 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
else:
break
if self.__modified:
try:
while True:
ticket = self.failManager.toBan()
self.jail.putFailTicket(ticket)
except FailManagerEmpty:
self.failManager.cleanup(MyTime.time())
self.performBan()
self.__modified = 0
except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop...
break

View file

@ -42,6 +42,32 @@ def asip(ip):
return ip
return IPAddr(ip)
def getfqdn(name=''):
"""Get fully-qualified hostname of given host, thereby resolve of an external
IPs and name will be preferred before the local domain (or a loopback), see gh-2438
"""
try:
name = name or socket.gethostname()
names = (
ai[3] for ai in socket.getaddrinfo(
name, None, 0, socket.SOCK_DGRAM, 0, socket.AI_CANONNAME
) if ai[3]
)
if names:
# first try to find a fqdn starting with the host name like www.domain.tld for www:
pref = name+'.'
first = None
for ai in names:
if ai.startswith(pref):
return ai
if not first: first = ai
# not found - simply use first known fqdn:
return first
except socket.error:
pass
# fallback to python's own getfqdn routine:
return socket.getfqdn(name)
##
# Utils class for DNS handling.
@ -132,7 +158,7 @@ class DNSUtils:
if name is None:
name = ''
for hostname in (
(socket.getfqdn, socket.gethostname) if fqdn else (socket.gethostname, socket.getfqdn)
(getfqdn, socket.gethostname) if fqdn else (socket.gethostname, getfqdn)
):
try:
name = hostname()
@ -176,6 +202,11 @@ class DNSUtils:
DNSUtils.CACHE_nameToIp.set(key, ips)
return ips
@staticmethod
def IPv6IsAllowed():
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs())
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
##
# Class for IP address handling.
@ -197,7 +228,7 @@ class IPAddr(object):
__slots__ = '_family','_addr','_plen','_maskplen','_raw'
# todo: make configurable the expired time and max count of cache entries:
CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60)
CACHE_OBJ = Utils.Cache(maxCount=10000, maxTime=5*60)
CIDR_RAW = -2
CIDR_UNSPEC = -1
@ -205,6 +236,10 @@ class IPAddr(object):
FAM_IPv6 = CIDR_RAW - socket.AF_INET6
def __new__(cls, ipstr, cidr=CIDR_UNSPEC):
if cidr == IPAddr.CIDR_RAW: # don't cache raw
ip = super(IPAddr, cls).__new__(cls)
ip.__init(ipstr, cidr)
return ip
# check already cached as IPAddr
args = (ipstr, cidr)
ip = IPAddr.CACHE_OBJ.get(args)
@ -221,7 +256,8 @@ class IPAddr(object):
return ip
ip = super(IPAddr, cls).__new__(cls)
ip.__init(ipstr, cidr)
IPAddr.CACHE_OBJ.set(args, ip)
if ip._family != IPAddr.CIDR_RAW:
IPAddr.CACHE_OBJ.set(args, ip)
return ip
@staticmethod

View file

@ -24,10 +24,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y
__license__ = "GPL"
import logging
import math
import random
import Queue
from .actions import Actions
from ..helpers import getLogger, extractOptions, MyTime
from ..helpers import getLogger, _as_bool, extractOptions, MyTime
from .mytime import MyTime
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -75,6 +78,8 @@ class Jail(object):
self.__name = name
self.__queue = Queue.Queue()
self.__filter = None
# Extra parameters for increase ban time
self._banExtra = {};
logSys.info("Creating new jail '%s'" % self.name)
if backend is not None:
self._setBackend(backend)
@ -193,8 +198,8 @@ class Jail(object):
Used by filter to add a failure for banning.
"""
self.__queue.put(ticket)
if not ticket.restored and self.database is not None:
self.database.addBan(self, ticket)
# add ban to database moved to observer (should previously check not already banned
# and increase ticket time if "bantime.increment" set)
def getFailTicket(self):
"""Get a fail ticket from the jail.
@ -207,15 +212,80 @@ class Jail(object):
except Queue.Empty:
return False
def restoreCurrentBans(self):
def setBanTimeExtra(self, opt, value):
# merge previous extra with new option:
be = self._banExtra;
if value == '':
value = None
if value is not None:
be[opt] = value;
elif opt in be:
del be[opt]
logSys.info('Set banTime.%s = %s', opt, value)
if opt == 'increment':
be[opt] = _as_bool(value)
if be.get(opt) and self.database is None:
logSys.warning("ban time increment is not available as long jail database is not set")
if opt in ['maxtime', 'rndtime']:
if not value is None:
be[opt] = MyTime.str2seconds(value)
# prepare formula lambda:
if opt in ['formula', 'factor', 'maxtime', 'rndtime', 'multipliers'] or be.get('evformula', None) is None:
# split multifiers to an array begins with 0 (or empty if not set):
if opt == 'multipliers':
be['evmultipliers'] = [int(i) for i in (value.split(' ') if value is not None and value != '' else [])]
# if we have multifiers - use it in lambda, otherwise compile and use formula within lambda
multipliers = be.get('evmultipliers', [])
banFactor = eval(be.get('factor', "1"))
if len(multipliers):
evformula = lambda ban, banFactor=banFactor: (
ban.Time * banFactor * multipliers[ban.Count if ban.Count < len(multipliers) else -1]
)
else:
formula = be.get('formula', 'ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor')
formula = compile(formula, '~inline-conf-expr~', 'eval')
evformula = lambda ban, banFactor=banFactor, formula=formula: max(ban.Time, eval(formula))
# extend lambda with max time :
if not be.get('maxtime', None) is None:
maxtime = be['maxtime']
evformula = lambda ban, evformula=evformula: min(evformula(ban), maxtime)
# mix lambda with random time (to prevent bot-nets to calculate exact time IP can be unbanned):
if not be.get('rndtime', None) is None:
rndtime = be['rndtime']
evformula = lambda ban, evformula=evformula: (evformula(ban) + random.random() * rndtime)
# set to extra dict:
be['evformula'] = evformula
#logSys.info('banTimeExtra : %s' % json.dumps(be))
def getBanTimeExtra(self, opt=None):
if opt is not None:
return self._banExtra.get(opt, None)
return self._banExtra
def getMaxBanTime(self):
"""Returns max possible ban-time of jail.
"""
return self._banExtra.get("maxtime", -1) \
if self._banExtra.get('increment') else self.actions.getBanTime()
def restoreCurrentBans(self, correctBanTime=True):
"""Restore any previous valid bans from the database.
"""
try:
if self.database is not None:
forbantime = self.actions.getBanTime()
for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime):
#logSys.debug('restored ticket: %s', ticket)
if not self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True):
if self._banExtra.get('increment'):
forbantime = None;
if correctBanTime:
correctBanTime = self.getMaxBanTime()
else:
# use ban time as search time if we have not enabled a increasing:
forbantime = self.actions.getBanTime()
for ticket in self.database.getCurrentBans(jail=self, forbantime=forbantime,
correctBanTime=correctBanTime, maxmatches=self.filter.failManager.maxMatches
):
try:
#logSys.debug('restored ticket: %s', ticket)
if self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True): continue
# mark ticked was restored from database - does not put it again into db:
ticket.restored = True
# correct start time / ban time (by the same end of ban):
@ -226,11 +296,13 @@ class Jail(object):
# ignore obsolete tickets:
if btm != -1 and btm <= 0:
continue
ticket.setTime(MyTime.time())
ticket.setBanTime(btm)
self.putFailTicket(ticket)
except Exception as e: # pragma: no cover
logSys.error('Restore ticket failed: %s', e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
except Exception as e: # pragma: no cover
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.error('Restore bans failed: %s', e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def start(self):
"""Start the jail, by starting filter and actions threads.

View file

@ -29,7 +29,7 @@ from threading import Thread
from abc import abstractmethod
from .utils import Utils
from ..helpers import excepthook
from ..helpers import excepthook, prctl_set_th_name
class JailThread(Thread):
@ -76,6 +76,15 @@ class JailThread(Thread):
print(e)
self.run = run_with_except_hook
if sys.version_info >= (3,): # pragma: 2.x no cover
def _bootstrap(self):
prctl_set_th_name(self.name)
return super(JailThread, self)._bootstrap();
else: # pragma: 3.x no cover
def __bootstrap(self):
prctl_set_th_name(self.name)
return Thread._Thread__bootstrap(self)
@abstractmethod
def status(self, flavor="basic"): # pragma: no cover - abstract
"""Abstract - Should provide status information.
@ -108,4 +117,6 @@ class JailThread(Thread):
if self.active is not None:
super(JailThread, self).join()
## python 2.x replace binding of private __bootstrap method:
if sys.version_info < (3,): # pragma: 3.x no cover
JailThread._Thread__bootstrap = JailThread._JailThread__bootstrap

View file

@ -113,6 +113,16 @@ class MyTime:
return time.localtime(x)
else:
return time.localtime(MyTime.myTime)
@staticmethod
def time2str(unixTime, format="%Y-%m-%d %H:%M:%S"):
"""Convert time to a string representing as date and time using given format.
Default format is ISO 8601, YYYY-MM-DD HH:MM:SS without microseconds.
@return ISO-capable string representation of given unixTime
"""
return datetime.datetime.fromtimestamp(
unixTime).replace(microsecond=0).strftime(format)
## precreate/precompile primitives used in str2seconds:

529
fail2ban/server/observer.py Normal file
View file

@ -0,0 +1,529 @@
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: t -*-
# vi: set ft=python sts=4 ts=4 sw=4 noet :
# This file is part of Fail2Ban.
#
# Fail2Ban is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Fail2Ban is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
# Author: Serg G. Brester (sebres)
#
# This module was written as part of ban time increment feature.
__author__ = "Serg G. Brester (sebres)"
__copyright__ = "Copyright (c) 2014 Serg G. Brester"
__license__ = "GPL"
import threading
from .jailthread import JailThread
from .failmanager import FailManagerEmpty
import os, logging, time, datetime, math, json, random
import sys
from ..helpers import getLogger
from .mytime import MyTime
from .utils import Utils
# Gets the instance of the logger.
logSys = getLogger(__name__)
class ObserverThread(JailThread):
"""Handles observing a database, managing bad ips and ban increment.
Parameters
----------
Attributes
----------
daemon
ident
name
status
active : bool
Control the state of the thread.
idle : bool
Control the idle state of the thread.
sleeptime : int
The time the thread sleeps for in the loop.
"""
# observer is event driven and it sleep organized incremental, so sleep intervals can be shortly:
DEFAULT_SLEEP_INTERVAL = Utils.DEFAULT_SLEEP_INTERVAL / 10
def __init__(self):
# init thread
super(ObserverThread, self).__init__(name='f2b/observer')
# before started - idle:
self.idle = True
## Event queue
self._queue_lock = threading.RLock()
self._queue = []
## Event, be notified if anything added to event queue
self._notify = threading.Event()
## Sleep for max 60 seconds, it possible to specify infinite to always sleep up to notifying via event,
## but so we can later do some service "events" occurred infrequently directly in main loop of observer (not using queue)
self.sleeptime = 60
#
self._timers = {}
self._paused = False
self.__db = None
self.__db_purge_interval = 60*60
# observer is a not main thread:
self.daemon = True
def __getitem__(self, i):
try:
return self._queue[i]
except KeyError:
raise KeyError("Invalid event index : %s" % i)
def __delitem__(self, name):
try:
del self._queue[i]
except KeyError:
raise KeyError("Invalid event index: %s" % i)
def __iter__(self):
return iter(self._queue)
def __len__(self):
return len(self._queue)
def __eq__(self, other): # Required for Threading
return False
def __hash__(self): # Required for Threading
return id(self)
def add_named_timer(self, name, starttime, *event):
"""Add a named timer event to queue will start (and wake) in 'starttime' seconds
Previous timer event with same name will be canceled and trigger self into
queue after new 'starttime' value
"""
t = self._timers.get(name, None)
if t is not None:
t.cancel()
t = threading.Timer(starttime, self.add, event)
self._timers[name] = t
t.start()
def add_timer(self, starttime, *event):
"""Add a timer event to queue will start (and wake) in 'starttime' seconds
"""
# in testing we should wait (looping) for the possible time drifts:
if MyTime.myTime is not None and starttime:
# test time after short sleep:
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
(MyTime.time() + starttime, time.time() + starttime, event)
)
t.start()
return
# add timer event:
t = threading.Timer(starttime, self.add, event)
t.start()
def _delayedEvent(self, endMyTime, endTime, event):
if MyTime.time() >= endMyTime or time.time() >= endTime:
self.add_timer(0, *event)
return
# repeat after short sleep:
t = threading.Timer(Utils.DEFAULT_SLEEP_INTERVAL, self._delayedEvent,
(endMyTime, endTime, event)
)
t.start()
def pulse_notify(self):
"""Notify wakeup (sets /and resets/ notify event)
"""
if not self._paused and self._notify:
self._notify.set()
#self._notify.clear()
def add(self, *event):
"""Add a event to queue and notify thread to wake up.
"""
## lock and add new event to queue:
with self._queue_lock:
self._queue.append(event)
self.pulse_notify()
def add_wn(self, *event):
"""Add a event to queue withouth notifying thread to wake up.
"""
## lock and add new event to queue:
with self._queue_lock:
self._queue.append(event)
def call_lambda(self, l, *args):
l(*args)
def run(self):
"""Main loop for Threading.
This function is the main loop of the thread.
Returns
-------
bool
True when the thread exits nicely.
"""
logSys.info("Observer start...")
## first time create named timer to purge database each hour (clean old entries) ...
self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge')
## Mapping of all possible event types of observer:
__meth = {
# universal lambda:
'call': self.call_lambda,
# system and service events:
'db_set': self.db_set,
'db_purge': self.db_purge,
# service events of observer self:
'is_alive' : self.isAlive,
'is_active': self.isActive,
'start': self.start,
'stop': self.stop,
'nop': lambda:(),
'shutdown': lambda:()
}
try:
## check it self with sending is_alive event
self.add('is_alive')
## if we should stop - break a main loop
while self.active:
self.idle = False
## check events available and execute all events from queue
while not self._paused:
## lock, check and pop one from begin of queue:
try:
ev = None
with self._queue_lock:
if len(self._queue):
ev = self._queue.pop(0)
if ev is None:
break
## retrieve method by name
meth = ev[0]
if not callable(ev[0]): meth = __meth.get(meth) or getattr(self, meth)
## execute it with rest of event as variable arguments
meth(*ev[1:])
except Exception as e:
#logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
logSys.error('%s', e, exc_info=True)
## going sleep, wait for events (in queue)
n = self._notify
if n:
self.idle = True
n.wait(self.sleeptime)
## wake up - reset signal now (we don't need it so long as we reed from queue)
n.clear()
if self._paused:
continue
else:
## notify event deleted (shutdown) - just sleep a litle bit (waiting for shutdown events, prevent high cpu usage)
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
## stop by shutdown and empty queue :
if not self.is_full:
break
## end of main loop - exit
logSys.info("Observer stopped, %s events remaining.", len(self._queue))
#print("Observer stopped, %s events remaining." % len(self._queue))
except Exception as e:
logSys.error('Observer stopped after error: %s', e, exc_info=True)
#print("Observer stopped with error: %s" % str(e))
# clear all events - exit, for possible calls of wait_empty:
with self._queue_lock:
self._queue = []
self.idle = True
return True
def isAlive(self):
#logSys.debug("Observer alive...")
return True
def isActive(self, fromStr=None):
# logSys.info("Observer alive, %s%s",
# 'active' if self.active else 'inactive',
# '' if fromStr is None else (", called from '%s'" % fromStr))
return self.active
def start(self):
with self._queue_lock:
if not self.active:
super(ObserverThread, self).start()
def stop(self):
if self.active and self._notify:
wtime = 5
logSys.info("Observer stop ... try to end queue %s seconds", wtime)
#print("Observer stop ....")
# just add shutdown job to make possible wait later until full (events remaining)
with self._queue_lock:
self.add_wn('shutdown')
#don't pulse - just set, because we will delete it hereafter (sometimes not wakeup)
n = self._notify
self._notify.set()
#self.pulse_notify()
self._notify = None
# wait max wtime seconds until full (events remaining)
self.wait_empty(wtime)
n.clear()
self.active = False
self.wait_idle(0.5)
@property
def is_full(self):
with self._queue_lock:
return True if len(self._queue) else False
def wait_empty(self, sleeptime=None):
"""Wait observer is running and returns if observer has no more events (queue is empty)
"""
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
if sleeptime is not None:
e = MyTime.time() + sleeptime
# block queue with not operation to be sure all really jobs are executed if nop goes from queue :
if self._notify is not None:
self.add_wn('nop')
if self.is_full and self.idle:
self.pulse_notify()
while self.is_full:
if sleeptime is not None and MyTime.time() > e:
break
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
# wait idle to be sure the last queue element is processed (because pop event before processing it) :
self.wait_idle(0.001)
return not self.is_full
def wait_idle(self, sleeptime=None):
"""Wait observer is running and returns if observer idle (observer sleeps)
"""
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
if self.idle:
return True
if sleeptime is not None:
e = MyTime.time() + sleeptime
while not self.idle:
if sleeptime is not None and MyTime.time() > e:
break
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
return self.idle
@property
def paused(self):
return self._paused;
@paused.setter
def paused(self, pause):
if self._paused == pause:
return
self._paused = pause
# wake after pause ended
self.pulse_notify()
@property
def status(self):
"""Status of observer to be implemented. [TODO]
"""
return ('', '')
## -----------------------------------------
## [Async] database service functionality ...
## -----------------------------------------
def db_set(self, db):
self.__db = db
def db_purge(self):
logSys.debug("Purge database event occurred")
if self.__db is not None:
self.__db.purge()
# trigger timer again ...
self.add_named_timer('DB_PURGE', self.__db_purge_interval, 'db_purge')
## -----------------------------------------
## [Async] ban time increment functionality ...
## -----------------------------------------
def failureFound(self, failManager, jail, ticket):
""" Notify observer a failure for ip was found
Observer will check ip was known (bad) and possibly increase an retry count
"""
# check jail active :
if not jail.isAlive() or not jail.getBanTimeExtra("increment"):
return
ip = ticket.getIP()
unixTime = ticket.getTime()
logSys.debug("[%s] Observer: failure found %s", jail.name, ip)
# increase retry count for known (bad) ip, corresponding banCount of it (one try will count than 2, 3, 5, 9 ...) :
banCount = 0
retryCount = 1
timeOfBan = None
try:
maxRetry = failManager.getMaxRetry()
db = jail.database
if db is not None:
for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail):
banCount = max(banCount, ticket.getBanCount())
retryCount = ((1 << (banCount if banCount < 20 else 20))/2 + 1)
# if lastBanTime == -1 or timeOfBan + lastBanTime * 2 > MyTime.time():
# retryCount = maxRetry
break
retryCount = min(retryCount, maxRetry)
# check this ticket already known (line was already processed and in the database and will be restored from there):
if timeOfBan is not None and unixTime <= timeOfBan:
logSys.debug("[%s] Ignore failure %s before last ban %s < %s, restored",
jail.name, ip, unixTime, timeOfBan)
return
# for not increased failures observer should not add it to fail manager, because was already added by filter self
if retryCount <= 1:
return
# retry counter was increased - add it again:
logSys.info("[%s] Found %s, bad - %s, %s # -> %s%s", jail.name, ip,
MyTime.time2str(unixTime), banCount, retryCount,
(', Ban' if retryCount >= maxRetry else ''))
# retryCount-1, because a ticket was already once incremented by filter self
retryCount = failManager.addFailure(ticket, retryCount - 1, True)
ticket.setBanCount(banCount)
# after observe we have increased attempt count, compare it >= maxretry ...
if retryCount >= maxRetry:
# perform the banning of the IP now (again)
# [todo]: this code part will be used multiple times - optimize it later.
try: # pragma: no branch - exception is the only way out
while True:
ticket = failManager.toBan(ip)
jail.putFailTicket(ticket)
except FailManagerEmpty:
failManager.cleanup(MyTime.time())
except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
class BanTimeIncr:
def __init__(self, banTime, banCount):
self.Time = banTime
self.Count = banCount
def calcBanTime(self, jail, banTime, banCount):
be = jail.getBanTimeExtra()
return be['evformula'](self.BanTimeIncr(banTime, banCount))
def incrBanTime(self, jail, banTime, ticket):
"""Check for IP address to increment ban time (if was already banned).
Returns
-------
float
new ban time.
"""
# check jail active :
if not jail.isAlive() or not jail.database:
return banTime
be = jail.getBanTimeExtra()
ip = ticket.getIP()
orgBanTime = banTime
# check ip was already banned (increment time of ban):
try:
if banTime > 0 and be.get('increment', False):
# search IP in database and increase time if found:
for banCount, timeOfBan, lastBanTime in \
jail.database.getBan(ip, jail, overalljails=be.get('overalljails', False)) \
:
# increment count in ticket (if still not increased from banmanager, test-cases?):
if banCount >= ticket.getBanCount():
ticket.setBanCount(banCount+1)
logSys.debug('IP %s was already banned: %s #, %s', ip, banCount, timeOfBan);
# calculate new ban time
if banCount > 0:
banTime = be['evformula'](self.BanTimeIncr(banTime, banCount))
ticket.setBanTime(banTime)
# check current ticket time to prevent increasing for twice read tickets (restored from log file besides database after restart)
if ticket.getTime() > timeOfBan:
logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount,
MyTime.time2str(timeOfBan),
datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime))));
else:
ticket.restored = True
break
except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
return banTime
def banFound(self, ticket, jail, btime):
""" Notify observer a ban occured for ip
Observer will check ip was known (bad) and possibly increase/prolong a ban time
Secondary we will actualize the bans and bips (bad ip) in database
"""
if ticket.restored: # pragma: no cover (normally not resored tickets only)
return
try:
oldbtime = btime
ip = ticket.getIP()
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime)
# if not permanent and ban time was not set - check time should be increased:
if btime != -1 and ticket.getBanTime() is None:
btime = self.incrBanTime(jail, btime, ticket)
# if we should prolong ban time:
if btime == -1 or btime > oldbtime:
ticket.setBanTime(btime)
# if not permanent
if btime != -1:
bendtime = ticket.getTime() + btime
logtime = (datetime.timedelta(seconds=int(btime)),
MyTime.time2str(bendtime))
# check ban is not too old :
if bendtime < MyTime.time():
logSys.debug('Ignore old bantime %s', logtime[1])
return False
else:
logtime = ('permanent', 'infinite')
# if ban time was prolonged - log again with new ban time:
if btime != oldbtime:
logSys.notice("[%s] Increase Ban %s (%d # %s -> %s)", jail.name,
ip, ticket.getBanCount(), *logtime)
# delayed prolonging ticket via actions that expected this (not later than 10 sec):
logSys.log(5, "[%s] Observer: prolong %s in %s", jail.name, ip, (btime, oldbtime))
self.add_timer(min(10, max(0, btime - oldbtime - 5)), self.prolongBan, ticket, jail)
# add ticket to database, but only if was not restored (not already read from database):
if jail.database is not None and not ticket.restored:
# add to database always only after ban time was calculated an not yet already banned:
jail.database.addBan(jail, ticket)
except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
def prolongBan(self, ticket, jail):
""" Notify observer a ban occured for ip
Observer will check ip was known (bad) and possibly increase/prolong a ban time
Secondary we will actualize the bans and bips (bad ip) in database
"""
try:
btime = ticket.getBanTime()
ip = ticket.getIP()
logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime)
# prolong ticket via actions that expected this:
jail.actions._prolongBan(ticket)
except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# Global observer initial created in server (could be later rewriten via singleton)
class _Observers:
def __init__(self):
self.Main = None
Observers = _Observers()

View file

@ -32,13 +32,14 @@ import signal
import stat
import sys
from .observer import Observers, ObserverThread
from .jails import Jails
from .filter import FileFilter, JournalFilter
from .transmitter import Transmitter
from .asyncserver import AsyncServer, AsyncServerException
from .. import version
from ..helpers import getLogger, _as_bool, extractOptions, str2LogLevel, \
getVerbosityFormat, excepthook
getVerbosityFormat, excepthook, prctl_set_th_name
# Gets the instance of the logger.
logSys = getLogger(__name__)
@ -80,6 +81,8 @@ class Server:
'Linux': '/dev/log',
}
self.__prev_signals = {}
# replace real thread name with short process name (for top/ps/pstree or diagnostic):
prctl_set_th_name('f2b/server')
def __sigTERMhandler(self, signum, frame): # pragma: no cover - indirect tested
logSys.debug("Caught signal %d. Exiting", signum)
@ -94,7 +97,7 @@ class Server:
self.__prev_signals[s] = signal.getsignal(s)
signal.signal(s, new)
def start(self, sock, pidfile, force=False, conf={}):
def start(self, sock, pidfile, force=False, observer=True, conf={}):
# First set the mask to only allow access to owner
os.umask(0077)
# Second daemonize before logging etc, because it will close all handles:
@ -144,6 +147,12 @@ class Server:
except (OSError, IOError) as e: # pragma: no cover
logSys.error("Unable to create PID file: %s", e)
# Create observers and start it:
if observer:
if Observers.Main is None:
Observers.Main = ObserverThread()
Observers.Main.start()
# Start the communication
logSys.debug("Starting communication")
try:
@ -183,6 +192,10 @@ class Server:
for s, sh in self.__prev_signals.iteritems():
signal.signal(s, sh)
# Give observer a small chance to complete its work before exit
if Observers.Main is not None:
Observers.Main.stop()
# Now stop all the jails
self.stopAllJail()
@ -192,12 +205,17 @@ class Server:
self.__db.close()
self.__db = None
# Stop observer and exit
if Observers.Main is not None:
Observers.Main.stop()
Observers.Main = None
# Stop async
if self.__asyncServer is not None:
self.__asyncServer.stop()
self.__asyncServer = None
logSys.info("Exiting Fail2ban")
def addJail(self, name, backend):
addflg = True
if self.__reload_state.get(name) and self.__jails.exists(name):
@ -443,6 +461,12 @@ class Server:
def getUseDns(self, name):
return self.__jails[name].filter.getUseDns()
def setMaxMatches(self, name, value):
self.__jails[name].filter.failManager.maxMatches = value
def getMaxMatches(self, name):
return self.__jails[name].filter.failManager.maxMatches
def setMaxRetry(self, name, value):
self.__jails[name].filter.setMaxRetry(value)
@ -473,26 +497,49 @@ class Server:
def setBanTime(self, name, value):
self.__jails[name].actions.setBanTime(value)
def addAttemptIP(self, name, *args):
return self.__jails[name].filter.addAttempt(*args)
def setBanIP(self, name, value):
return self.__jails[name].filter.addBannedIP(value)
def setUnbanIP(self, name=None, value=None):
return self.__jails[name].actions.addBannedIP(value)
def setUnbanIP(self, name=None, value=None, ifexists=True):
if name is not None:
# in all jails:
# single jail:
jails = [self.__jails[name]]
else:
# single jail:
# in all jails:
jails = self.__jails.values()
# unban given or all (if value is None):
cnt = 0
ifexists |= (name is None)
for jail in jails:
cnt += jail.actions.removeBannedIP(value, ifexists=(name is None))
if value and not cnt:
logSys.info("%s is not banned", value)
cnt += jail.actions.removeBannedIP(value, ifexists=ifexists)
return cnt
def getBanTime(self, name):
return self.__jails[name].actions.getBanTime()
def getBanList(self, name, withTime=False):
"""Returns the list of banned IP addresses for a jail.
Parameters
----------
name : str
The name of a jail.
Returns
-------
list
The list of banned IP addresses.
"""
return self.__jails[name].actions.getBanList(withTime)
def setBanTimeExtra(self, name, opt, value):
self.__jails[name].setBanTimeExtra(opt, value)
def getBanTimeExtra(self, name, opt):
return self.__jails[name].getBanTimeExtra(opt)
def isStarted(self):
return self.__asyncServer is not None and self.__asyncServer.isActive()
@ -707,6 +754,16 @@ class Server:
logSys.info("flush performed on %s" % self.__logTarget)
return "flushed"
def setThreadOptions(self, value):
for o, v in value.iteritems():
if o == 'stacksize':
threading.stack_size(int(v)*1024)
else: # pragma: no cover
raise KeyError("unknown option %r" % o)
def getThreadOptions(self):
return {'stacksize': threading.stack_size() // 1024}
def setDatabase(self, filename):
# if not changed - nothing to do
if self.__db and self.__db.filename == filename:
@ -726,6 +783,8 @@ class Server:
logSys.error(
"Unable to import fail2ban database module as sqlite "
"is not available.")
if Observers.Main is not None:
Observers.Main.db_set(self.__db)
def getDatabase(self):
return self.__db

View file

@ -24,8 +24,6 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL"
import sys
from ..helpers import getLogger
from .ipdns import IPAddr
from .mytime import MyTime
@ -35,6 +33,7 @@ logSys = getLogger(__name__)
class Ticket(object):
__slots__ = ('_ip', '_flags', '_banCount', '_banTime', '_time', '_data', '_retry', '_lastReset')
MAX_TIME = 0X7FFFFFFFFFFF ;# 4461763-th year
@ -61,35 +60,44 @@ class Ticket(object):
self._data[k] = v
if ticket:
# ticket available - copy whole information from ticket:
self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
self.update(ticket)
#self.__dict__.update(i for i in ticket.__dict__.iteritems() if i[0] in self.__dict__)
def __str__(self):
return "%s: ip=%s time=%s #attempts=%d matches=%r" % \
(self.__class__.__name__.split('.')[-1], self.__ip, self._time,
self._data['failures'], self._data.get('matches', []))
return "%s: ip=%s time=%s bantime=%s bancount=%s #attempts=%d matches=%r" % \
(self.__class__.__name__.split('.')[-1], self._ip, self._time,
self._banTime, self._banCount,
self._data['failures'], self._data.get('matches', []))
def __repr__(self):
return str(self)
def __eq__(self, other):
try:
return self.__ip == other.__ip and \
return self._ip == other._ip and \
round(self._time, 2) == round(other._time, 2) and \
self._data == other._data
except AttributeError:
return False
def update(self, ticket):
for n in ticket.__slots__:
v = getattr(ticket, n, None)
if v is not None:
setattr(self, n, v)
def setIP(self, value):
# guarantee using IPAddr instead of unicode, str for the IP
if isinstance(value, basestring):
value = IPAddr(value)
self.__ip = value
self._ip = value
def getID(self):
return self._data.get('fid', self.__ip)
return self._data.get('fid', self._ip)
def getIP(self):
return self.__ip
return self._ip
def setTime(self, value):
self._time = value
@ -98,16 +106,17 @@ class Ticket(object):
return self._time
def setBanTime(self, value):
self._banTime = value;
self._banTime = value
def getBanTime(self, defaultBT=None):
return (self._banTime if self._banTime is not None else defaultBT)
def setBanCount(self, value):
self._banCount = value;
def setBanCount(self, value, always=False):
if always or value > self._banCount:
self._banCount = value
def incrBanCount(self, value = 1):
self._banCount += value;
def incrBanCount(self, value=1):
self._banCount += value
def getBanCount(self):
return self._banCount;
@ -135,7 +144,13 @@ class Ticket(object):
return self._data['failures']
def setMatches(self, matches):
self._data['matches'] = matches or []
if matches:
self._data['matches'] = matches
else:
try:
del self._data['matches']
except KeyError:
pass
def getMatches(self):
return [(line if not isinstance(line, (list, tuple)) else "".join(line)) \
@ -200,26 +215,33 @@ class Ticket(object):
# return single value of data:
return self._data.get(key, default)
@property
def banEpoch(self):
return getattr(self, '_banEpoch', 0)
@banEpoch.setter
def banEpoch(self, value):
self._banEpoch = value
class FailTicket(Ticket):
def __init__(self, ip=None, time=None, matches=None, data={}, ticket=None):
# this class variables:
self.__retry = 0
self.__lastReset = None
self._retry = 0
self._lastReset = None
# create/copy using default ticket constructor:
Ticket.__init__(self, ip, time, matches, data, ticket)
# init:
if ticket is None:
self.__lastReset = time if time is not None else self.getTime()
if not self.__retry:
self.__retry = self._data['failures'];
self._lastReset = time if time is not None else self.getTime()
if not self._retry:
self._retry = self._data['failures'];
def setRetry(self, value):
""" Set artificial retry count, normally equal failures / attempt,
used in incremental features (BanTimeIncr) to increase retry count for bad IPs
"""
self.__retry = value
self._retry = value
if not self._data['failures']:
self._data['failures'] = 1
if not value:
@ -230,10 +252,10 @@ class FailTicket(Ticket):
""" Returns failures / attempt count or
artificial retry count increased for bad IPs
"""
return max(self.__retry, self._data['failures'])
return max(self._retry, self._data['failures'])
def inc(self, matches=None, attempt=1, count=1):
self.__retry += count
self._retry += count
self._data['failures'] += attempt
if matches:
# we should duplicate "matches", because possibly referenced to multiple tickets:
@ -250,15 +272,24 @@ class FailTicket(Ticket):
return self._time
def getLastReset(self):
return self.__lastReset
return self._lastReset
def setLastReset(self, value):
self.__lastReset = value
self._lastReset = value
@staticmethod
def wrap(o):
o.__class__ = FailTicket
return o
##
# Ban Ticket.
#
# This class extends the Ticket class. It is mainly used by the BanManager.
class BanTicket(Ticket):
pass
class BanTicket(FailTicket):
@staticmethod
def wrap(o):
o.__class__ = BanTicket
return o

View file

@ -43,6 +43,7 @@ class Transmitter:
def __init__(self, server):
self.__server = server
self.__quiet = 0
##
# Proceeds a command.
@ -69,9 +70,10 @@ class Transmitter:
#
def __commandHandler(self, command):
if command[0] == "ping":
name = command[0]
if name == "ping":
return "pong"
elif command[0] == "add":
elif name == "add":
name = command[1]
if name == "--all":
raise Exception("Reserved name %r" % (name,))
@ -81,11 +83,15 @@ class Transmitter:
backend = "auto"
self.__server.addJail(name, backend)
return name
elif command[0] == "start":
elif name == "multi-set":
return self.__commandSet(command[1:], True)
elif name == "set":
return self.__commandSet(command[1:])
elif name == "start":
name = command[1]
self.__server.startJail(name)
return None
elif command[0] == "stop":
elif name == "stop":
if len(command) == 1:
self.__server.quit()
elif command[1] == "--all":
@ -94,47 +100,50 @@ class Transmitter:
name = command[1]
self.__server.stopJail(name)
return None
elif command[0] == "reload":
elif name == "reload":
opts = command[1:3]
self.__quiet = 1
try:
self.__server.reloadJails(*opts, begin=True)
for cmd in command[3]:
self.__commandHandler(cmd)
finally:
self.__quiet = 0
self.__server.reloadJails(*opts, begin=False)
return 'OK'
elif len(command) >= 2 and command[0] == "unban":
elif name == "unban" and len(command) >= 2:
# unban in all jails:
value = command[1:]
# if all ips:
if len(value) == 1 and value[0] == "--all":
return self.__server.setUnbanIP()
cnt = 0
for value in value:
cnt += self.__server.setUnbanIP(None, value)
return cnt
elif command[0] == "echo":
return self.__server.setUnbanIP(None, value)
elif name == "echo":
return command[1:]
elif command[0] == "server-status":
elif name == "server-status":
logSys.debug("Status: ready")
return "Server ready"
elif command[0] == "sleep":
elif name == "server-stream":
self.__quiet = 1
try:
for cmd in command[1]:
self.__commandHandler(cmd)
finally:
self.__quiet = 0
return None
elif name == "sleep":
value = command[1]
time.sleep(float(value))
return None
elif command[0] == "flushlogs":
elif name == "flushlogs":
return self.__server.flushLogs()
elif command[0] == "multi-set":
return self.__commandSet(command[1:], True)
elif command[0] == "set":
return self.__commandSet(command[1:])
elif command[0] == "get":
elif name == "get":
return self.__commandGet(command[1:])
elif command[0] == "status":
elif name == "status":
return self.status(command[1:])
elif command[0] == "version":
elif name == "version":
return version.version
elif command[0] == "config-error":
elif name == "config-error":
logSys.error(command[1])
return None
raise Exception("Invalid command")
@ -145,19 +154,26 @@ class Transmitter:
if name == "loglevel":
value = command[1]
self.__server.setLogLevel(value)
if self.__quiet: return
return self.__server.getLogLevel()
elif name == "logtarget":
value = command[1]
if self.__server.setLogTarget(value):
if self.__quiet: return
return self.__server.getLogTarget()
else:
raise Exception("Failed to change log target")
elif name == "syslogsocket":
value = command[1]
if self.__server.setSyslogSocket(value):
if self.__quiet: return
return self.__server.getSyslogSocket()
else:
raise Exception("Failed to change syslog socket")
#Thread
elif name == "thread":
value = command[1]
return self.__server.setThreadOptions(value)
#Database
elif name == "dbfile":
self.__server.setDatabase(command[1])
@ -165,14 +181,25 @@ class Transmitter:
if db is None:
return None
else:
if self.__quiet: return
return db.filename
elif name == "dbmaxmatches":
db = self.__server.getDatabase()
if db is None:
logSys.log(logging.MSG, "dbmaxmatches setting was not in effect since no db yet")
return None
else:
db.maxMatches = int(command[1])
if self.__quiet: return
return db.maxMatches
elif name == "dbpurgeage":
db = self.__server.getDatabase()
if db is None:
logSys.warning("dbpurgeage setting was not in effect since no db yet")
logSys.log(logging.MSG, "dbpurgeage setting was not in effect since no db yet")
return None
else:
db.purgeage = command[1]
if self.__quiet: return
return db.purgeage
# Jail
elif command[1] == "idle":
@ -182,27 +209,33 @@ class Transmitter:
self.__server.setIdleJail(name, False)
else:
raise Exception("Invalid idle option, must be 'on' or 'off'")
if self.__quiet: return
return self.__server.getIdleJail(name)
# Filter
elif command[1] == "ignoreself":
value = command[2]
self.__server.setIgnoreSelf(name, value)
if self.__quiet: return
return self.__server.getIgnoreSelf(name)
elif command[1] == "addignoreip":
value = command[2]
self.__server.addIgnoreIP(name, value)
for value in command[2:]:
self.__server.addIgnoreIP(name, value)
if self.__quiet: return
return self.__server.getIgnoreIP(name)
elif command[1] == "delignoreip":
value = command[2]
self.__server.delIgnoreIP(name, value)
if self.__quiet: return
return self.__server.getIgnoreIP(name)
elif command[1] == "ignorecommand":
value = command[2]
self.__server.setIgnoreCommand(name, value)
if self.__quiet: return
return self.__server.getIgnoreCommand(name)
elif command[1] == "ignorecache":
value = command[2]
self.__server.setIgnoreCache(name, value)
if self.__quiet: return
return self.__server.getIgnoreCache(name)
elif command[1] == "addlogpath":
value = command[2]
@ -215,88 +248,125 @@ class Transmitter:
elif len(command) > 4:
raise ValueError("Only one file can be added at a time")
self.__server.addLogPath(name, value, tail)
if self.__quiet: return
return self.__server.getLogPath(name)
elif command[1] == "dellogpath":
value = command[2]
self.__server.delLogPath(name, value)
if self.__quiet: return
return self.__server.getLogPath(name)
elif command[1] == "logencoding":
value = command[2]
self.__server.setLogEncoding(name, value)
if self.__quiet: return
return self.__server.getLogEncoding(name)
elif command[1] == "addjournalmatch": # pragma: systemd no cover
value = command[2:]
self.__server.addJournalMatch(name, value)
if self.__quiet: return
return self.__server.getJournalMatch(name)
elif command[1] == "deljournalmatch": # pragma: systemd no cover
value = command[2:]
self.__server.delJournalMatch(name, value)
if self.__quiet: return
return self.__server.getJournalMatch(name)
elif command[1] == "prefregex":
value = command[2]
self.__server.setPrefRegex(name, value)
if self.__quiet: return
return self.__server.getPrefRegex(name)
elif command[1] == "addfailregex":
value = command[2]
self.__server.addFailRegex(name, value, multiple=multiple)
if multiple:
return True
if self.__quiet: return
return self.__server.getFailRegex(name)
elif command[1] == "delfailregex":
value = int(command[2])
self.__server.delFailRegex(name, value)
if self.__quiet: return
return self.__server.getFailRegex(name)
elif command[1] == "addignoreregex":
value = command[2]
self.__server.addIgnoreRegex(name, value, multiple=multiple)
if multiple:
return True
if self.__quiet: return
return self.__server.getIgnoreRegex(name)
elif command[1] == "delignoreregex":
value = int(command[2])
self.__server.delIgnoreRegex(name, value)
if self.__quiet: return
return self.__server.getIgnoreRegex(name)
elif command[1] == "usedns":
value = command[2]
self.__server.setUseDns(name, value)
if self.__quiet: return
return self.__server.getUseDns(name)
elif command[1] == "findtime":
value = command[2]
self.__server.setFindTime(name, value)
if self.__quiet: return
return self.__server.getFindTime(name)
elif command[1] == "datepattern":
value = command[2]
self.__server.setDatePattern(name, value)
if self.__quiet: return
return self.__server.getDatePattern(name)
elif command[1] == "logtimezone":
value = command[2]
self.__server.setLogTimeZone(name, value)
if self.__quiet: return
return self.__server.getLogTimeZone(name)
elif command[1] == "maxmatches":
value = command[2]
self.__server.setMaxMatches(name, int(value))
if self.__quiet: return
return self.__server.getMaxMatches(name)
elif command[1] == "maxretry":
value = command[2]
self.__server.setMaxRetry(name, int(value))
if self.__quiet: return
return self.__server.getMaxRetry(name)
elif command[1] == "maxlines":
value = command[2]
self.__server.setMaxLines(name, int(value))
if self.__quiet: return
return self.__server.getMaxLines(name)
# command
elif command[1] == "bantime":
value = command[2]
self.__server.setBanTime(name, value)
if self.__quiet: return
return self.__server.getBanTime(name)
elif command[1] == "banip":
elif command[1] == "attempt":
value = command[2:]
if self.__quiet: return
return self.__server.addAttemptIP(name, *value)
elif command[1].startswith("bantime."):
value = command[2]
opt = command[1][len("bantime."):]
self.__server.setBanTimeExtra(name, opt, value)
if self.__quiet: return
return self.__server.getBanTimeExtra(name, opt)
elif command[1] == "banip":
value = command[2:]
return self.__server.setBanIP(name,value)
elif command[1] == "unbanip":
value = command[2]
self.__server.setUnbanIP(name, value)
return value
ifexists = True
if command[2] != "--report-absent":
value = command[2:]
else:
ifexists = False
value = command[3:]
return self.__server.setUnbanIP(name, value, ifexists=ifexists)
elif command[1] == "addaction":
args = [command[2]]
if len(command) > 3:
args.extend([command[3], json.loads(command[4])])
self.__server.addAction(name, *args)
if self.__quiet: return
return args[0]
elif command[1] == "delaction":
value = command[2]
@ -320,10 +390,12 @@ class Transmitter:
actionkey = command[3]
if callable(getattr(action, actionkey, None)):
actionvalue = json.loads(command[4]) if len(command)>4 else {}
if self.__quiet: return
return getattr(action, actionkey)(**actionvalue)
else:
actionvalue = command[4]
setattr(action, actionkey, actionvalue)
if self.__quiet: return
return getattr(action, actionkey)
raise Exception("Invalid command %r (no set action or not yet implemented)" % (command[1],))
@ -336,6 +408,9 @@ class Transmitter:
return self.__server.getLogTarget()
elif name == "syslogsocket":
return self.__server.getSyslogSocket()
#Thread
elif name == "thread":
return self.__server.getThreadOptions()
#Database
elif name == "dbfile":
db = self.__server.getDatabase()
@ -343,6 +418,12 @@ class Transmitter:
return None
else:
return db.filename
elif name == "dbmaxmatches":
db = self.__server.getDatabase()
if db is None:
return None
else:
return db.maxMatches
elif name == "dbpurgeage":
db = self.__server.getDatabase()
if db is None:
@ -378,6 +459,8 @@ class Transmitter:
return self.__server.getDatePattern(name)
elif command[1] == "logtimezone":
return self.__server.getLogTimeZone(name)
elif command[1] == "maxmatches":
return self.__server.getMaxMatches(name)
elif command[1] == "maxretry":
return self.__server.getMaxRetry(name)
elif command[1] == "maxlines":
@ -385,6 +468,12 @@ class Transmitter:
# Action
elif command[1] == "bantime":
return self.__server.getBanTime(name)
elif command[1] == "banip":
return self.__server.getBanList(name,
withTime=len(command) > 2 and command[2] == "--with-time")
elif command[1].startswith("bantime."):
opt = command[1][len("bantime."):]
return self.__server.getBanTimeExtra(name, opt)
elif command[1] == "actions":
return self.__server.getActions(name).keys()
elif command[1] == "action":

View file

@ -95,31 +95,35 @@ class Utils():
def set(self, k, v):
t = time.time()
cache = self._cache # for shorter local access
# clean cache if max count reached:
if len(cache) >= self.maxCount:
# avoid multiple modification of list multi-threaded:
with self.__lock:
if len(cache) >= self.maxCount:
for (ck, cv) in cache.items():
# avoid multiple modification of dict multi-threaded:
cache = self._cache
with self.__lock:
# clean cache if max count reached:
if len(cache) >= self.maxCount:
if OrderedDict is not dict:
# ordered (so remove some from ahead, FIFO)
while cache:
(ck, cv) = cache.popitem(last=False)
# if not yet expired (but has free slot for new entry):
if cv[1] > t and len(cache) < self.maxCount:
break
else: # pragma: 3.x no cover (dict is in 2.6 only)
remlst = []
for (ck, cv) in cache.iteritems():
# if expired:
if cv[1] <= t:
self.unset(ck)
elif OrderedDict is not dict:
break
remlst.append(ck)
for ck in remlst:
self._cache.pop(ck, None)
# if still max count - remove any one:
if len(cache) >= self.maxCount:
if OrderedDict is not dict: # first (older):
cache.popitem(False)
else: # pragma: 3.x no cover
cache.popitem()
cache[k] = (v, t + self.maxTime)
while cache and len(cache) >= self.maxCount:
cache.popitem()
# set now:
cache[k] = (v, t + self.maxTime)
def unset(self, k):
try:
del self._cache[k]
except KeyError:
pass
with self.__lock:
self._cache.pop(k, None)
@staticmethod
@ -240,8 +244,8 @@ class Utils():
return False if not output else (False, stdout, stderr, retcode)
std_level = logging.DEBUG if retcode in success_codes else logging.ERROR
if std_level > logSys.getEffectiveLevel():
if logCmd: logCmd(std_level-1); logCmd = None
if std_level >= logSys.getEffectiveLevel():
if logCmd: logCmd(std_level-1 if std_level == logging.DEBUG else logging.ERROR); logCmd = None
# if we need output (to return or to log it):
if output or std_level >= logSys.getEffectiveLevel():
@ -346,7 +350,7 @@ class Utils():
return e.errno == errno.EPERM
else:
return True
else: # pragma : no cover (no windows currently supported)
else: # pragma: no cover (no windows currently supported)
@staticmethod
def pid_exists(pid):
import ctypes

View file

@ -55,6 +55,7 @@ if sys.version_info >= (2,7): # pragma: no cover - may be unavailable
pythonModule = None
modAction = None
@skip_if_not_available
def setUp(self):
"""Call before every test case."""
super(BadIPsActionTest, self).setUp()
@ -82,7 +83,7 @@ if sys.version_info >= (2,7): # pragma: no cover - may be unavailable
'banaction': "test",
'age': "2w",
'score': 5,
'key': "fail2ban-test-suite",
#'key': "fail2ban-test-suite",
#'bankey': "fail2ban-test-suite",
'timeout': (3 if unittest.F2B.fast else 60),
})

View file

@ -28,11 +28,10 @@ import time
import os
import tempfile
from ..server.actions import Actions
from ..server.ticket import FailTicket
from ..server.utils import Utils
from .dummyjail import DummyJail
from .utils import LogCaptureTestCase
from .utils import LogCaptureTestCase, with_alt_time, with_tmpdir, MyTime
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
@ -43,21 +42,21 @@ class ExecuteActions(LogCaptureTestCase):
"""Call before every test case."""
super(ExecuteActions, self).setUp()
self.__jail = DummyJail()
self.__actions = Actions(self.__jail)
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp()
self.__actions = self.__jail.actions
def tearDown(self):
super(ExecuteActions, self).tearDown()
os.remove(self.__tmpfilename)
def defaultActions(self):
def defaultAction(self, o={}):
self.__actions.add('ip')
self.__ip = self.__actions['ip']
self.__ip.actionstart = 'echo ip start 64 >> "%s"' % self.__tmpfilename
self.__ip.actionban = 'echo ip ban <ip> >> "%s"' % self.__tmpfilename
self.__ip.actionunban = 'echo ip unban <ip> >> "%s"' % self.__tmpfilename
self.__ip.actioncheck = 'echo ip check <ip> >> "%s"' % self.__tmpfilename
self.__ip.actionstop = 'echo ip stop >> "%s"' % self.__tmpfilename
act = self.__actions['ip']
act.actionstart = 'echo ip start'+o.get('start', '')
act.actionban = 'echo ip ban <ip>'+o.get('ban', '')
act.actionunban = 'echo ip unban <ip>'+o.get('unban', '')
act.actioncheck = 'echo ip check'+o.get('check', '')
act.actionflush = 'echo ip flush'+o.get('flush', '')
act.actionstop = 'echo ip stop'+o.get('stop', '')
return act
def testActionsAddDuplicateName(self):
self.__actions.add('test')
@ -78,14 +77,23 @@ class ExecuteActions(LogCaptureTestCase):
self.assertEqual(self.__actions.getBanTime(),127)
self.assertRaises(ValueError, self.__actions.removeBannedIP, '127.0.0.1')
def testActionsOutput(self):
self.defaultActions()
self.__actions.start()
with open(self.__tmpfilename) as f:
self.assertTrue( Utils.wait_for(lambda: (f.read() == "ip start 64\n"), 3) )
def testAddBannedIP(self):
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertLogged('Ban 192.0.2.1')
self.pruneLog()
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '192.0.2.2', '192.0.2.3']), 2)
self.assertLogged('192.0.2.1 already banned')
self.assertNotLogged('Ban 192.0.2.1')
self.assertLogged('Ban 192.0.2.2')
self.assertLogged('Ban 192.0.2.3')
def testActionsOutput(self):
self.defaultAction()
self.__actions.start()
self.assertLogged("stdout: %r" % 'ip start', wait=True)
self.__actions.stop()
self.__actions.join()
self.assertLogged("stdout: %r" % 'ip flush', "stdout: %r" % 'ip stop')
self.assertEqual(self.__actions.status(),[("Currently banned", 0 ),
("Total banned", 0 ), ("Banned IP list", [] )])
@ -149,7 +157,7 @@ class ExecuteActions(LogCaptureTestCase):
"action2",
os.path.join(TEST_FILES_DIR, "action.d/action_modifyainfo.py"),
{})
self.__jail.putFailTicket(FailTicket("1.2.3.4", 0))
self.__jail.putFailTicket(FailTicket("1.2.3.4"))
self.__actions._Actions__checkBan()
# Will fail if modification of aInfo from first action propagates
# to second action, as both delete same key
@ -163,3 +171,334 @@ class ExecuteActions(LogCaptureTestCase):
self.assertNotLogged("Failed to execute unban")
self.assertLogged("action1 unban deleted aInfo IP")
self.assertLogged("action2 unban deleted aInfo IP")
@with_alt_time
def testUnbanOnBusyBanBombing(self):
# check unban happens in-between of "ban bombing" despite lower precedence,
# if it is not work, we'll not see "Unbanned 30" (rather "Unbanned 50")
# because then all the unbans occur earliest at flushing (after stop)
# each 3rd ban we should see an unban check (and up to 5 tickets gets unbanned):
self.__actions.banPrecedence = 3
self.__actions.unbanMaxCount = 5
self.__actions.setBanTime(100)
self.__actions.start()
MyTime.setTime(0); # avoid "expired bantime" (in 0.11)
i = 0
while i < 20:
ip = "192.0.2.%d" % i
self.__jail.putFailTicket(FailTicket(ip, 0))
i += 1
# wait for last ban (all 20 tickets gets banned):
self.assertLogged(' / 20,', wait=True)
MyTime.setTime(200); # unban time for 20 tickets reached
while i < 50:
ip = "192.0.2.%d" % i
self.__jail.putFailTicket(FailTicket(ip, 200))
i += 1
# wait for last ban (all 50 tickets gets banned):
self.assertLogged(' / 50,', wait=True)
self.__actions.stop()
self.__actions.join()
self.assertLogged('Unbanned 30, 0 ticket(s)')
self.assertNotLogged('Unbanned 50, 0 ticket(s)')
def testActionsConsistencyCheck(self):
act = self.defaultAction({'check':' <family>', 'flush':' <family>'})
# flush for inet6 is intentionally "broken" here - test no unhandled except and invariant check:
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
act.actionstart_on_demand = True
self.__actions.start()
self.assertNotLogged("stdout: %r" % 'ip start')
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start',
"stdout: %r" % 'ip ban 192.0.2.1',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
# check should fail (so cause stop/start):
self.pruneLog('[test-phase 1a] simulate inconsistent irreparable env by unban')
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.__actions.removeBannedIP('2001:db8::1')
self.assertLogged('Invariant check failed. Unban is impossible.',
wait=True)
self.pruneLog('[test-phase 1b] simulate inconsistent irreparable env by flush')
self.__actions._Actions__flushBan()
self.assertLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip flush inet6',
'Failed to flush bans',
'No flush occurred, do consistency check',
'Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop', # same for both families
'Failed to flush bans',
all=True, wait=True)
# check succeeds:
self.pruneLog('[test-phase 2] consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 2001:db8::1',
"stdout: %r" % 'ip start', # same for both families
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
self.assertNotLogged("stdout: %r" % 'ip check inet4',
all=True)
self.pruneLog('[test-phase 3] failed flush in consistent env')
self.__actions._Actions__flushBan()
self.assertLogged('Failed to flush bans',
'No flush occurred, do consistency check',
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip check inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip stop',
"stdout: %r" % 'ip start',
'Unable to restore environment',
all=True)
# stop, flush succeeds:
self.pruneLog('[test-phase end] flush successful')
act['actionflush?family=inet6'] = act.actionflush
self.__actions.stop()
self.__actions.join()
self.assertLogged(
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip stop', # same for both families
'action ip terminated',
all=True, wait=True)
# no flush for inet4 (already successfully flushed):
self.assertNotLogged("ERROR",
"stdout: %r" % 'ip flush inet4',
'Unban tickets each individualy',
all=True)
def testActionsConsistencyCheckDiffFam(self):
# same as testActionsConsistencyCheck, but different start/stop commands for both families and repair on unban
act = self.defaultAction({'start':' <family>', 'check':' <family>', 'flush':' <family>', 'stop':' <family>'})
# flush for inet6 is intentionally "broken" here - test no unhandled except and invariant check:
act['actionflush?family=inet6'] = act.actionflush + '; exit 1'
act.actionstart_on_demand = True
act.actionrepair_on_unban = True
self.__actions.start()
self.assertNotLogged("stdout: %r" % 'ip start')
self.assertEqual(self.__actions.addBannedIP('192.0.2.1'), 1)
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip ban 192.0.2.1',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
# check should fail (so cause stop/start):
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.pruneLog('[test-phase 1a] simulate inconsistent irreparable env by unban')
self.__actions.removeBannedIP('2001:db8::1')
self.assertLogged('Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip start inet6', # start on demand (not on repair)
"stdout: %r" % 'ip stop inet4', # family inet4 is not affected
"stdout: %r" % 'ip start inet4',
all=True)
self.pruneLog('[test-phase 1b] simulate inconsistent irreparable env by ban')
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip check inet6',
'Unable to restore environment',
'Failed to execute ban',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip stop inet4', # family inet4 is not affected
"stdout: %r" % 'ip start inet4',
all=True)
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::2'), 1)
act['actioncheck?family=inet6'] = act.actioncheck + '; exit 1'
self.pruneLog('[test-phase 1c] simulate inconsistent irreparable env by flush')
self.__actions._Actions__flushBan()
self.assertLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip flush inet6',
'Failed to flush bans',
'No flush occurred, do consistency check',
'Invariant check failed. Trying to restore a sane environment',
"stdout: %r" % 'ip stop inet6',
'Failed to flush bans in jail',
all=True, wait=True)
# start/stop should be called for inet6 only:
self.assertNotLogged(
"stdout: %r" % 'ip stop inet4',
all=True)
# check succeeds:
self.pruneLog('[test-phase 2] consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.assertEqual(self.__actions.addBannedIP('2001:db8::1'), 1)
self.assertLogged('Ban 2001:db8::1',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip check inet4',
"stdout: %r" % 'ip start inet4',
all=True)
self.pruneLog('[test-phase 3] failed flush in consistent env')
act['actioncheck?family=inet6'] = act.actioncheck
self.__actions._Actions__flushBan()
self.assertLogged('Failed to flush bans',
'No flush occurred, do consistency check',
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip check inet6',
all=True, wait=True)
self.assertNotLogged(
"stdout: %r" % 'ip flush inet4',
"stdout: %r" % 'ip stop inet4',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip stop inet6',
"stdout: %r" % 'ip start inet6',
all=True)
# stop, flush succeeds:
self.pruneLog('[test-phase end] flush successful')
act['actionflush?family=inet6'] = act.actionflush
self.__actions.stop()
self.__actions.join()
self.assertLogged(
"stdout: %r" % 'ip flush inet6',
"stdout: %r" % 'ip stop inet4',
"stdout: %r" % 'ip stop inet6',
'action ip terminated',
all=True, wait=True)
# no flush for inet4 (already successfully flushed):
self.assertNotLogged("ERROR",
"stdout: %r" % 'ip flush inet4',
'Unban tickets each individualy',
all=True)
@with_alt_time
@with_tmpdir
def testActionsRebanBrokenAfterRepair(self, tmp):
act = self.defaultAction({
'start':' <family>; touch "<FN>"',
'check':' <family>; test -f "<FN>"',
'flush':' <family>; echo -n "" > "<FN>"',
'stop': ' <family>; rm -f "<FN>"',
'ban': ' <family>; echo "<ip> <family>" >> "<FN>"',
})
act['FN'] = tmp+'/<family>'
act.actionstart_on_demand = True
act.actionrepair = 'echo ip repair <family>; touch "<FN>"'
act.actionreban = 'echo ip reban <ip> <family>; echo "<ip> <family> -- rebanned" >> "<FN>"'
self.pruneLog('[test-phase 0] initial ban')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1']), 2)
self.assertLogged('Ban 192.0.2.1', 'Ban 2001:db8::1',
"stdout: %r" % 'ip start inet4',
"stdout: %r" % 'ip ban 192.0.2.1 inet4',
"stdout: %r" % 'ip start inet6',
"stdout: %r" % 'ip ban 2001:db8::1 inet6',
all=True)
self.pruneLog('[test-phase 1] check ban')
self.dumpFile(tmp+'/inet4')
self.assertLogged('192.0.2.1 inet4')
self.assertNotLogged('2001:db8::1 inet6')
self.pruneLog()
self.dumpFile(tmp+'/inet6')
self.assertLogged('2001:db8::1 inet6')
self.assertNotLogged('192.0.2.1 inet4')
# simulate 3 seconds past:
MyTime.setTime(MyTime.time() + 4)
# already banned produces events:
self.pruneLog('[test-phase 2] check already banned')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1', '2001:db8::2']), 1)
self.assertLogged(
'192.0.2.1 already banned', '2001:db8::1 already banned', 'Ban 2001:db8::2',
"stdout: %r" % 'ip check inet4', # both checks occurred
"stdout: %r" % 'ip check inet6',
all=True)
self.dumpFile(tmp+'/inet4')
self.dumpFile(tmp+'/inet6')
# no reban should occur:
self.assertNotLogged('Reban 192.0.2.1', 'Reban 2001:db8::1',
"stdout: %r" % 'ip ban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip ban 2001:db8::1 inet6',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
'192.0.2.1 inet4 -- repaired',
'2001:db8::1 inet6 -- repaired',
all=True)
# simulate 3 seconds past:
MyTime.setTime(MyTime.time() + 4)
# break env (remove both files, so check would fail):
os.remove(tmp+'/inet4')
os.remove(tmp+'/inet6')
# test again already banned (it shall cause reban now):
self.pruneLog('[test-phase 3a] check reban after sane env repaired')
self.assertEqual(self.__actions.addBannedIP(['192.0.2.1', '2001:db8::1']), 2)
self.assertLogged(
"Invariant check failed. Trying to restore a sane environment",
"stdout: %r" % 'ip repair inet4', # both repairs occurred
"stdout: %r" % 'ip repair inet6',
"Reban 192.0.2.1, action 'ip'", "Reban 2001:db8::1, action 'ip'", # both rebans also
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
all=True)
# now last IP (2001:db8::2) - no repair, but still old epoch of ticket, so it gets rebanned:
self.pruneLog('[test-phase 3a] check reban by epoch mismatch (without repair)')
self.assertEqual(self.__actions.addBannedIP('2001:db8::2'), 1)
self.assertLogged(
"Reban 2001:db8::2, action 'ip'",
"stdout: %r" % 'ip reban 2001:db8::2 inet6',
all=True)
self.assertNotLogged(
"Invariant check failed. Trying to restore a sane environment",
"stdout: %r" % 'ip repair inet4', # both repairs occurred
"stdout: %r" % 'ip repair inet6',
"Reban 192.0.2.1, action 'ip'", "Reban 2001:db8::1, action 'ip'", # both rebans also
"stdout: %r" % 'ip reban 192.0.2.1 inet4',
"stdout: %r" % 'ip reban 2001:db8::1 inet6',
all=True)
# and bans present in files:
self.pruneLog('[test-phase 4] check reban')
self.dumpFile(tmp+'/inet4')
self.assertLogged('192.0.2.1 inet4 -- rebanned')
self.assertNotLogged('2001:db8::1 inet6 -- rebanned')
self.pruneLog()
self.dumpFile(tmp+'/inet6')
self.assertLogged(
'2001:db8::1 inet6 -- rebanned',
'2001:db8::2 inet6 -- rebanned', all=True)
self.assertNotLogged('192.0.2.1 inet4 -- rebanned')
# coverage - intended error in reban (no unhandled exception, message logged):
act.actionreban = ''
act.actionban = 'exit 1'
self.assertEqual(self.__actions._Actions__reBan(FailTicket("192.0.2.1", 0)), 0)
self.assertLogged(
'Failed to execute reban',
'Error banning 192.0.2.1', all=True)

View file

@ -34,8 +34,8 @@ from ..server.actions import OrderedDict, Actions
from ..server.utils import Utils
from .dummyjail import DummyJail
from .utils import LogCaptureTestCase
from .utils import pid_exists
from .utils import pid_exists, with_tmpdir, LogCaptureTestCase
class CommandActionTest(LogCaptureTestCase):
@ -206,16 +206,16 @@ class CommandActionTest(LogCaptureTestCase):
"Text 890 text 123 ABC")
self.assertEqual(
self.__action.replaceTag("<matches>",
{'matches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
{'matches': "some >char< should \\< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
self.assertEqual(
self.__action.replaceTag("<ipmatches>",
{'ipmatches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
{'ipmatches': "some >char< should \\< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\n")
self.assertEqual(
self.__action.replaceTag("<ipjailmatches>",
{'ipjailmatches': "some >char< should \< be[ escap}ed&\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\n")
{'ipjailmatches': "some >char< should \\< be[ escap}ed&\r\n"}),
"some \\>char\\< should \\\\\\< be\\[ escap\\}ed\\&\\r\\n")
# Recursive
aInfo["ABC"] = "<xyz>"
@ -297,18 +297,21 @@ class CommandActionTest(LogCaptureTestCase):
"Text 000-567 text 567 '567'")
self.assertTrue(len(cache) >= 3)
def testExecuteActionBan(self):
self.__action.actionstart = "touch /tmp/fail2ban.test"
self.assertEqual(self.__action.actionstart, "touch /tmp/fail2ban.test")
self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.assertEqual(self.__action.actionstop, 'rm -f /tmp/fail2ban.test')
@with_tmpdir
def testExecuteActionBan(self, tmp):
tmp += "/fail2ban.test"
self.__action.actionstart = "touch '%s'" % tmp
self.__action.actionrepair = self.__action.actionstart
self.assertEqual(self.__action.actionstart, "touch '%s'" % tmp)
self.__action.actionstop = "rm -f '%s'" % tmp
self.assertEqual(self.__action.actionstop, "rm -f '%s'" % tmp)
self.__action.actionban = "echo -n"
self.assertEqual(self.__action.actionban, 'echo -n')
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.assertEqual(self.__action.actioncheck, '[ -e /tmp/fail2ban.test ]')
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.assertEqual(self.__action.actioncheck, "[ -e '%s' ]" % tmp)
self.__action.actionunban = "true"
self.assertEqual(self.__action.actionunban, 'true')
self.pruneLog()
self.assertNotLogged('returned')
# no action was actually executed yet
@ -316,42 +319,66 @@ class CommandActionTest(LogCaptureTestCase):
self.__action.ban({'ip': None})
self.assertLogged('Invariant check failed')
self.assertLogged('returned successfully')
self.__action.stop()
self.assertLogged(self.__action.actionstop)
def testExecuteActionEmptyUnban(self):
# unban will be executed for actions with banned items only:
self.__action.actionban = ""
self.__action.actionunban = ""
self.__action.actionflush = "echo -n 'flush'"
self.__action.actionstop = "echo -n 'stop'"
self.__action.start();
self.__action.ban({});
self.pruneLog()
self.__action.unban({})
self.assertLogged('Nothing to do')
self.assertLogged('Nothing to do', wait=True)
# same as above but with interim flush, so no unban anymore:
self.__action.ban({});
self.pruneLog('[phase 2]')
self.__action.flush()
self.__action.unban({})
self.__action.stop()
self.assertLogged('stop', wait=True)
self.assertNotLogged('Nothing to do')
def testExecuteActionStartCtags(self):
@with_tmpdir
def testExecuteActionStartCtags(self, tmp):
tmp += '/fail2ban.test'
self.__action.HOST = "192.0.2.0"
self.__action.actionstart = "touch /tmp/fail2ban.test.<HOST>"
self.__action.actionstop = "rm -f /tmp/fail2ban.test.<HOST>"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test.192.0.2.0 ]"
self.__action.actionstart = "touch '%s.<HOST>'" % tmp
self.__action.actionstop = "rm -f '%s.<HOST>'" % tmp
self.__action.actioncheck = "[ -e '%s.192.0.2.0' ]" % tmp
self.__action.start()
self.__action.consistencyCheck()
def testExecuteActionCheckRestoreEnvironment(self):
@with_tmpdir
def testExecuteActionCheckRestoreEnvironment(self, tmp):
tmp += '/fail2ban.test'
self.__action.actionstart = ""
self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionstop = "rm -f '%s'" % tmp
self.__action.actionban = "rm '%s'" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.assertRaises(RuntimeError, self.__action.ban, {'ip': None})
self.assertLogged('Invariant check failed', 'Unable to restore environment', all=True)
# 2nd time, try to restore with producing error in stop, but succeeded start hereafter:
self.pruneLog('[phase 2]')
self.__action.actionstart = "touch /tmp/fail2ban.test"
self.__action.actionstop = "rm /tmp/fail2ban.test"
self.__action.actionban = 'printf "%%b\n" <ip> >> /tmp/fail2ban.test'
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionstart = "touch '%s'" % tmp
self.__action.actionstop = "rm '%s'" % tmp
self.__action.actionban = """printf "%%%%b\n" <ip> >> '%s'""" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.__action.ban({'ip': None})
self.assertLogged('Invariant check failed')
self.assertNotLogged('Unable to restore environment')
def testExecuteActionCheckRepairEnvironment(self):
@with_tmpdir
def testExecuteActionCheckRepairEnvironment(self, tmp):
tmp += '/fail2ban.test'
self.__action.actionstart = ""
self.__action.actionstop = ""
self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.__action.actionrepair = "echo 'repair ...'; touch /tmp/fail2ban.test"
self.__action.actionban = "rm '%s'" % tmp
self.__action.actioncheck = "[ -e '%s' ]" % tmp
self.__action.actionrepair = "echo 'repair ...'; touch '%s'" % tmp
# 1st time with success repair:
self.__action.ban({'ip': None})
self.assertLogged("Invariant check failed. Trying", "echo 'repair ...'", all=True)
@ -379,13 +406,13 @@ class CommandActionTest(LogCaptureTestCase):
'user': "tester"
}
})
self.__action.actionban = "touch /tmp/fail2ban.test.123; echo 'failure <F-ID> of <F-USER> -<F-TEST>- from <ip>:<F-PORT>'"
self.__action.actionunban = "rm /tmp/fail2ban.test.<ABC>; echo 'user <F-USER> unbanned'"
self.__action.actionban = "echo '<ABC>, failure <F-ID> of <F-USER> -<F-TEST>- from <ip>:<F-PORT>'"
self.__action.actionunban = "echo '<ABC>, user <F-USER> unbanned'"
self.__action.ban(aInfo)
self.__action.unban(aInfo)
self.assertLogged(
" -- stdout: 'failure 111 of tester -- from 192.0.2.1:222'",
" -- stdout: 'user tester unbanned'",
" -- stdout: '123, failure 111 of tester -- from 192.0.2.1:222'",
" -- stdout: '123, user tester unbanned'",
all=True
)
@ -560,6 +587,19 @@ class CommandActionTest(LogCaptureTestCase):
self.assertEqual(len(m), 3)
self.assertIn('c', m)
self.assertEqual((m['a'], m['b'], m['c']), (5, 11, 'test'))
# immutability of copy:
m['d'] = 'dddd'
m2 = m.copy()
m2['c'] = lambda self: self['a'] + 7
m2['a'] = 1
del m2['b']
del m2['d']
self.assertTrue('b' in m)
self.assertTrue('d' in m)
self.assertFalse('b' in m2)
self.assertFalse('d' in m2)
self.assertEqual((m['a'], m['b'], m['c'], m['d']), (5, 11, 'test', 'dddd'))
self.assertEqual((m2['a'], m2['c']), (1, 8))
def testCallingMapRep(self):
m = CallingMap({

View file

@ -26,6 +26,8 @@ __license__ = "GPL"
import unittest
from .utils import setUpMyTime, tearDownMyTime
from ..server.banmanager import BanManager
from ..server.ticket import BanTicket
@ -33,12 +35,14 @@ class AddFailure(unittest.TestCase):
def setUp(self):
"""Call before every test case."""
super(AddFailure, self).setUp()
setUpMyTime()
self.__ticket = BanTicket('193.168.0.128', 1167605999.0)
self.__banManager = BanManager()
def tearDown(self):
"""Call after every test case."""
super(AddFailure, self).tearDown()
tearDownMyTime()
def testAdd(self):
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
@ -93,6 +97,25 @@ class AddFailure(unittest.TestCase):
self.assertTrue(self.__banManager.addBanTicket(self.__ticket))
ticket = BanTicket('111.111.1.111', 1167605999.0)
self.assertFalse(self.__banManager._inBanList(ticket))
def testBanTimeIncr(self):
ticket = BanTicket(self.__ticket.getIP(), self.__ticket.getTime())
## increase twice and at end permanent, check time/count increase:
c = 0
for i in (1000, 2000, -1):
self.__banManager.addBanTicket(self.__ticket); c += 1
ticket.setBanTime(i)
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), i, c))
## after permanent, it should remain permanent ban time (-1):
self.__banManager.addBanTicket(self.__ticket); c += 1
ticket.setBanTime(-1)
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
ticket.setBanTime(1000)
self.assertFalse(self.__banManager.addBanTicket(ticket)); # no incr of c (already banned)
self.assertEqual(str(self.__banManager.getTicketByID(ticket.getIP())),
"BanTicket: ip=%s time=%s bantime=%s bancount=%s #attempts=0 matches=[]" % (ticket.getIP(), ticket.getTime(), -1, c))
def testUnban(self):
btime = self.__banManager.getBanTime()
@ -137,6 +160,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
"""Call before every test case."""
super(StatusExtendedCymruInfo, self).setUp()
unittest.F2B.SkipIfNoNetwork()
setUpMyTime()
self.__ban_ip = "93.184.216.34"
self.__asn = "15133"
self.__country = "EU"
@ -148,6 +172,7 @@ class StatusExtendedCymruInfo(unittest.TestCase):
def tearDown(self):
"""Call after every test case."""
super(StatusExtendedCymruInfo, self).tearDown()
tearDownMyTime()
available = True, None

View file

@ -31,7 +31,7 @@ import unittest
from ..client.configreader import ConfigReader, ConfigReaderUnshared, \
DefinitionInitConfigReader, NoSectionError
from ..client import configparserinc
from ..client.jailreader import JailReader, extractOptions
from ..client.jailreader import JailReader, extractOptions, splitWithOptions
from ..client.filterreader import FilterReader
from ..client.jailsreader import JailsReader
from ..client.actionreader import ActionReader, CommandAction
@ -302,6 +302,32 @@ class JailReaderTest(LogCaptureTestCase):
self.assertEqual(jail.getName(), 'sshd')
jail.setName('ssh-funky-blocker')
self.assertEqual(jail.getName(), 'ssh-funky-blocker')
def testOverrideFilterOptInJail(self):
unittest.F2B.SkipIfCfgMissing(stock=True); # expected include of common.conf
jail = JailReader('sshd-override-flt-opts', basedir=IMPERFECT_CONFIG,
share_config=IMPERFECT_CONFIG_SHARE_CFG, force_enable=True)
self.assertTrue(jail.read())
self.assertTrue(jail.getOptions())
self.assertTrue(jail.isEnabled())
stream = jail.convert()
# check filter options are overriden with values specified directly in jail:
# prefregex:
self.assertEqual([['set', 'sshd-override-flt-opts', 'prefregex', '^Test']],
[o for o in stream if len(o) > 2 and o[2] == 'prefregex'])
# journalmatch:
self.assertEqual([['set', 'sshd-override-flt-opts', 'addjournalmatch', '_COMM=test']],
[o for o in stream if len(o) > 2 and o[2] == 'addjournalmatch'])
# maxlines:
self.assertEqual([['set', 'sshd-override-flt-opts', 'maxlines', 2]],
[o for o in stream if len(o) > 2 and o[2] == 'maxlines'])
# usedns should be before all regex in jail stream:
usednsidx = stream.index(['set', 'sshd-override-flt-opts', 'usedns', 'no'])
i = 0
for o in stream:
self.assertFalse(len(o) > 2 and o[2].endswith('regex'))
i += 1
if i > usednsidx: break
def testSplitOption(self):
# Simple example
@ -752,7 +778,7 @@ class JailsReaderTest(LogCaptureTestCase):
# somewhat duplicating here what is done in JailsReader if
# the jail is enabled
for act in actions.split('\n'):
for act in splitWithOptions(actions):
actName, actOpt = extractOptions(act)
self.assertTrue(len(actName))
self.assertTrue(isinstance(actOpt, dict))
@ -878,21 +904,23 @@ class JailsReaderTest(LogCaptureTestCase):
% (option, commands))
# Set up of logging should come first
self.assertEqual(find_set('syslogsocket'), 0)
self.assertEqual(find_set('loglevel'), 1)
self.assertEqual(find_set('logtarget'), 2)
# then dbfile should be before dbpurgeage
self.assertTrue(
find_set('syslogsocket') < find_set('loglevel') < find_set('logtarget')
)
# then dbfile should be before dbmaxmatches and dbpurgeage
self.assertTrue(find_set('dbpurgeage') > find_set('dbfile'))
self.assertTrue(find_set('dbmaxmatches') > find_set('dbfile'))
# and there is logging information left to be passed into the
# server
self.assertSortedEqual(commands,
[['set', 'dbfile',
'/var/lib/fail2ban/fail2ban.sqlite3'],
['set', 'dbpurgeage', '1d'],
['set', 'loglevel', "INFO"],
['set', 'logtarget', '/var/log/fail2ban.log'],
['set', 'syslogsocket', 'auto']])
self.assertSortedEqual(commands,[
['set', 'syslogsocket', 'auto'],
['set', 'loglevel', "INFO"],
['set', 'logtarget', '/var/log/fail2ban.log'],
['set', 'dbfile', '/var/lib/fail2ban/fail2ban.sqlite3'],
['set', 'dbmaxmatches', 10],
['set', 'dbpurgeage', '1d'],
])
# and if we force change configurator's fail2ban's baseDir
# there should be an error message (test visually ;) --

View file

@ -56,6 +56,7 @@ cmnfailre = ^%(__prefix_line_sl)s[aA]uthentication (?:failure|error|failed) for
mdre-normal =
mdre-ddos = ^%(__prefix_line_sl)sDid not receive identification string from <HOST>
^%(__prefix_line_sl)sBad protocol version identification '.*' from <HOST>
^%(__prefix_line_sl)sConnection closed by%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
^%(__prefix_line_sl)sConnection reset by <HOST>
^%(__prefix_line_ml1)sSSH: Server;Ltype: (?:Authname|Version|Kex);Remote: <HOST>-\d+;[A-Z]\w+:.*%(__prefix_line_ml2)sRead from socket failed: Connection reset by peer%(__suff)s$

View file

@ -62,4 +62,15 @@ log2nd = %(logpath)s
d.log
action = action[actname='ban']
action[actname='log', logpath="%(log2nd)s"]
action[actname='test']
action[actname='test']
[sshd-override-flt-opts]
filter = zzz-sshd-obsolete-multiline[logtype=short]
backend = systemd
prefregex = ^Test
failregex = ^Test unused <ADDR>$
ignoreregex = ^Test ignore <ADDR>$
journalmatch = _COMM=test
maxlines = 2
usedns = no
enabled = false

View file

@ -164,10 +164,47 @@ class DatabaseTest(LogCaptureTestCase):
self.assertEqual(self.db.updateDb(Fail2BanDb.__version__), Fail2BanDb.__version__)
self.assertRaises(NotImplementedError, self.db.updateDb, Fail2BanDb.__version__ + 1)
# check current bans (should find exactly 1 ticket after upgrade):
tickets = self.db.getCurrentBans(fromtime=1388009242, correctBanTime=123456)
self.assertEqual(len(tickets), 1)
self.assertEqual(tickets[0].getBanTime(), 123456); # ban-time was unknown (normally updated from jail)
finally:
if self.db and self.db._dbFilename != ":memory:":
os.remove(self.db._dbBackupFilename)
def testUpdateDb2(self):
self.db = None
if self.dbFilename is None: # pragma: no cover
_, self.dbFilename = tempfile.mkstemp(".db", "fail2ban_")
shutil.copyfile(
os.path.join(TEST_FILES_DIR, 'database_v2.db'), self.dbFilename)
self.db = Fail2BanDb(self.dbFilename)
self.assertEqual(self.db.getJailNames(), set(['pam-generic']))
self.assertEqual(self.db.getLogPaths(), set(['/var/log/auth.log']))
bans = self.db.getBans()
self.assertEqual(len(bans), 2)
# compare first ticket completely:
ticket = FailTicket("1.2.3.7", 1417595494, [
u'Dec 3 09:31:08 f2btest test:auth[27658]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7',
u'Dec 3 09:31:32 f2btest test:auth[27671]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7',
u'Dec 3 09:31:34 f2btest test:auth[27673]: pam_unix(test:auth): authentication failure; logname= uid=0 euid=0 tty=test ruser= rhost=1.2.3.7'
])
ticket.setAttempt(3)
self.assertEqual(bans[0], ticket)
# second ban found also:
self.assertEqual(bans[1].getIP(), "1.2.3.8")
# updated ?
self.assertEqual(self.db.updateDb(Fail2BanDb.__version__), Fail2BanDb.__version__)
# check current bans (should find 2 tickets after upgrade):
self.jail = DummyJail(name='pam-generic')
tickets = self.db.getCurrentBans(jail=self.jail, fromtime=1417595494)
self.assertEqual(len(tickets), 2)
self.assertEqual(tickets[0].getBanTime(), 600)
# further update should fail:
self.assertRaises(NotImplementedError, self.db.updateDb, Fail2BanDb.__version__ + 1)
# clean:
os.remove(self.db._dbBackupFilename)
def testAddJail(self):
self.jail = DummyJail()
self.db.addJail(self.jail)
@ -331,9 +368,9 @@ class DatabaseTest(LogCaptureTestCase):
# be returned
self.assertEqual(len(self.db.getBans(jail=self.jail,bantime=-1)), 2)
def testGetBansMerged_MaxEntries(self):
def testGetBansMerged_MaxMatches(self):
self.testAddJail()
maxEntries = 2
maxMatches = 2
failures = [
{"matches": ["abc\n"], "user": set(['test'])},
{"matches": ["123\n"], "user": set(['test'])},
@ -349,34 +386,49 @@ class DatabaseTest(LogCaptureTestCase):
ticket.setAttempt(1)
self.db.addBan(self.jail, ticket)
# should retrieve 2 matches only, but count of all attempts:
self.db.maxEntries = maxEntries;
self.db.maxMatches = maxMatches;
ticket = self.db.getBansMerged("127.0.0.1")
self.assertEqual(ticket.getIP(), "127.0.0.1")
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# add more failures at once:
ticket = FailTicket("127.0.0.1", MyTime.time() - 10, matches2find,
data={"user": set(['test', 'root'])})
ticket.setAttempt(len(failures))
self.db.addBan(self.jail, ticket)
# should retrieve 2 matches only, but count of all attempts:
self.db.maxEntries = maxEntries;
ticket = self.db.getBansMerged("127.0.0.1")
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# also using getCurrentBans:
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100)
self.assertTrue(ticket is not None)
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), matches2find[-maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), matches2find[-maxMatches:])
# maxmatches of jail < dbmaxmatches (so read 1 match and 0 matches):
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100,
maxmatches=1)
self.assertEqual(len(ticket.getMatches()), 1)
self.assertEqual(ticket.getMatches(), failures[3]['matches'])
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100,
maxmatches=0)
self.assertEqual(len(ticket.getMatches()), 0)
# dbmaxmatches = 0, should retrieve 0 matches by last ban:
ticket.setMatches(["1","2","3"])
self.db.maxMatches = 0;
self.db.addBan(self.jail, ticket)
ticket = self.db.getCurrentBans(self.jail, "127.0.0.1", fromtime=MyTime.time()-100)
self.assertTrue(ticket is not None)
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), 0)
def testGetBansMerged(self):
self.testAddJail()
jail2 = DummyJail()
jail2 = DummyJail(name='DummyJail-2')
self.db.addJail(jail2)
ticket = FailTicket("127.0.0.1", MyTime.time() - 40, ["abc\n"])
@ -458,10 +510,25 @@ class DatabaseTest(LogCaptureTestCase):
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=15,
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
self.assertEqual(len(tickets), 0)
# persistent bantime (-1), so never expired:
# persistent bantime (-1), so never expired (but no persistent tickets):
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
self.assertEqual(len(tickets), 2)
self.assertEqual(len(tickets), 0)
# add persistent one:
ticket.setBanTime(-1)
self.db.addBan(self.jail, ticket)
# persistent bantime (-1), so never expired (but jail has other max bantime now):
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
# no tickets should be found (max ban time = 600):
self.assertEqual(len(tickets), 0)
self.assertLogged("ignore ticket (with new max ban-time %r)" % self.jail.getMaxBanTime())
# change jail to persistent ban and try again (1 persistent ticket):
self.jail.actions.setBanTime(-1)
tickets = self.db.getCurrentBans(jail=self.jail, forbantime=-1,
fromtime=MyTime.time() + MyTime.str2seconds("1year"))
self.assertEqual(len(tickets), 1)
self.assertEqual(tickets[0].getBanTime(), -1); # current jail ban time.
def testActionWithDB(self):
# test action together with database functionality
@ -472,8 +539,9 @@ class DatabaseTest(LogCaptureTestCase):
"action_checkainfo",
os.path.join(TEST_FILES_DIR, "action.d/action_checkainfo.py"),
{})
ticket = FailTicket("1.2.3.4", MyTime.time(), ['test', 'test'])
ticket = FailTicket("1.2.3.4")
ticket.setAttempt(5)
ticket.setMatches(['test', 'test'])
self.jail.putFailTicket(ticket)
actions._Actions__checkBan()
self.assertLogged("ban ainfo %s, %s, %s, %s" % (True, True, True, True))

View file

@ -103,7 +103,7 @@ class DateDetectorTest(LogCaptureTestCase):
def testGetEpochPattern(self):
self.__datedetector = DateDetector()
self.__datedetector.appendTemplate('(?<=\|\s){LEPOCH}(?=\s\|)')
self.__datedetector.appendTemplate(r'(?<=\|\s){LEPOCH}(?=\s\|)')
# correct short/long epoch time, using all variants:
for fact in (1, 1000, 1000000):
for dateUnix in (1138049999, 32535244799):
@ -385,7 +385,7 @@ class DateDetectorTest(LogCaptureTestCase):
self.assertRaises(Exception, t.getDate, 'no date line')
iso8601 = DatePatternRegex("%Y-%m-%d[T ]%H:%M:%S(?:\.%f)?%z")
iso8601 = DatePatternRegex(r"%Y-%m-%d[T ]%H:%M:%S(?:\.%f)?%z")
class CustomDateFormatsTest(unittest.TestCase):

View file

@ -28,15 +28,20 @@ from ..server.jail import Jail
from ..server.actions import Actions
class DummyActions(Actions):
def checkBan(self):
return self._Actions__checkBan()
class DummyJail(Jail):
"""A simple 'jail' to suck in all the tickets generated by Filter's
"""
def __init__(self, backend=None):
def __init__(self, name='DummyJail', backend=None):
self.lock = Lock()
self.queue = []
super(DummyJail, self).__init__(name='DummyJail', backend=backend)
super(DummyJail, self).__init__(name=name, backend=backend)
self.__db = None
self.__actions = Actions(self)
self.__actions = DummyActions(self)
def __len__(self):
with self.lock:
@ -61,10 +66,6 @@ class DummyJail(Jail):
except IndexError:
return False
@property
def name(self):
return "DummyJail #%s with %d tickets" % (id(self), len(self))
@property
def idle(self):
return False;

View file

@ -44,7 +44,7 @@ from ..server import server
from ..server.mytime import MyTime
from ..server.utils import Utils
from .utils import LogCaptureTestCase, logSys as DefLogSys, with_tmpdir, shutil, logging, \
STOCK, CONFIG_DIR as STOCK_CONF_DIR
STOCK, CONFIG_DIR as STOCK_CONF_DIR, TEST_NOW, tearDownMyTime
from ..helpers import getLogger
@ -78,6 +78,35 @@ fail2banclient.output = \
fail2banserver.output = \
protocol.output = _test_output
def _time_shift(shift):
# jump to the future (+shift minutes):
logSys.debug("===>>> time shift + %s min", shift)
MyTime.setTime(MyTime.time() + shift*60)
Observers = server.Observers
def _observer_wait_idle():
"""Helper to wait observer becomes idle"""
if Observers.Main is not None:
Observers.Main.wait_empty(MID_WAITTIME)
Observers.Main.wait_idle(MID_WAITTIME / 5)
def _observer_wait_before_incrban(cond, timeout=MID_WAITTIME):
"""Helper to block observer before increase bantime until some condition gets true"""
if Observers.Main is not None:
# switch ban handler:
_obs_banFound = Observers.Main.banFound
def _banFound(*args, **kwargs):
# restore original handler:
Observers.Main.banFound = _obs_banFound
# wait for:
logSys.debug(' [Observer::banFound] *** observer blocked for test')
Utils.wait_for(cond, timeout)
logSys.debug(' [Observer::banFound] +++ observer runs again')
# original banFound:
_obs_banFound(*args, **kwargs)
Observers.Main.banFound = _banFound
#
# Mocking .exit so we could test its correct operation.
@ -113,16 +142,7 @@ fail2banclient.input_command = _test_input_command
fail2bancmdline.PRODUCTION = \
fail2banserver.PRODUCTION = False
def _out_file(fn, handle=logSys.debug):
"""Helper which outputs content of the file at HEAVYDEBUG loglevels"""
if (handle != logSys.debug or logSys.getEffectiveLevel() <= logging.DEBUG):
handle('---- ' + fn + ' ----')
for line in fileinput.input(fn):
line = line.rstrip('\n')
handle(line)
handle('-'*30)
_out_file = LogCaptureTestCase.dumpFile
def _write_file(fn, mode, *lines):
f = open(fn, mode)
@ -140,11 +160,13 @@ def _read_file(fn):
def _start_params(tmp, use_stock=False, use_stock_cfg=None,
logtarget="/dev/null", db=":memory:", jails=("",), create_before_start=None
logtarget="/dev/null", db=":memory:", f2b_local=(), jails=("",),
create_before_start=None,
):
cfg = pjoin(tmp, "config")
if db == 'auto':
db = pjoin(tmp, "f2b-db.sqlite3")
j_conf = 'jail.conf'
if use_stock and STOCK:
# copy config (sub-directories as alias):
def ig_dirs(dir, files):
@ -166,6 +188,8 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None,
if r.match(line):
line = "backend = polling"
print(line)
# jails to local:
j_conf = 'jail.local' if jails else ''
else:
# just empty config directory without anything (only fail2ban.conf/jail.conf):
os.mkdir(cfg)
@ -178,18 +202,28 @@ def _start_params(tmp, use_stock=False, use_stock_cfg=None,
"pidfile = " + pjoin(tmp, "f2b.pid"),
"backend = polling",
"dbfile = " + db,
"dbmaxmatches = 100",
"dbpurgeage = 1d",
"",
)
_write_file(pjoin(cfg, "jail.conf"), "w",
# write jails (local or conf):
if j_conf:
_write_file(pjoin(cfg, j_conf), "w",
*((
"[INCLUDES]", "",
"[DEFAULT]", "tmp = " + tmp, "",
)+jails)
)
if unittest.F2B.log_level < logging.DEBUG: # pragma: no cover
_out_file(pjoin(cfg, "fail2ban.conf"))
_out_file(pjoin(cfg, "jail.conf"))
if f2b_local:
_write_file(pjoin(cfg, "fail2ban.local"), "w", *f2b_local)
if unittest.F2B.log_level < logging.DEBUG: # pragma: no cover
_out_file(pjoin(cfg, "fail2ban.conf"))
_out_file(pjoin(cfg, "jail.conf"))
if f2b_local:
_out_file(pjoin(cfg, "fail2ban.local"))
if j_conf and j_conf != "jail.conf":
_out_file(pjoin(cfg, j_conf))
# link stock actions and filters:
if use_stock_cfg and STOCK:
for n in use_stock_cfg:
@ -348,6 +382,7 @@ def with_foreground_server_thread(startextra={}):
# so don't kill (same process) - if success, just wait for end of worker:
if phase.get('end', None):
th.join()
tearDownMyTime()
return wrapper
return _deco_wrapper
@ -374,6 +409,7 @@ class Fail2banClientServerBase(LogCaptureTestCase):
server.DEF_LOGTARGET = SRV_DEF_LOGTARGET
server.DEF_LOGLEVEL = SRV_DEF_LOGLEVEL
LogCaptureTestCase.tearDown(self)
tearDownMyTime()
@staticmethod
def _test_exit(code=0):
@ -431,8 +467,16 @@ class Fail2banClientServerBase(LogCaptureTestCase):
phase['end'] = True
logSys.debug("end of test worker")
@with_foreground_server_thread()
@with_foreground_server_thread(startextra={'f2b_local':(
"[Thread]",
"stacksize = 32"
"",
)})
def testStartForeground(self, tmp, startparams):
# check thread options were set:
self.pruneLog()
self.execCmd(SUCCESS, startparams, "get", "thread")
self.assertLogged("{'stacksize': 32}")
# several commands to server:
self.execCmd(SUCCESS, startparams, "ping")
self.execCmd(FAILED, startparams, "~~unknown~cmd~failed~~")
@ -822,7 +866,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
"usedns = no",
"maxretry = 3",
"findtime = 10m",
"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
r"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
"datepattern = {^LN-BEG}EPOCH",
"ignoreip = 127.0.0.1/8 ::1", # just to cover ignoreip in jailreader/transmitter
"",
@ -838,8 +882,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
"logpath = " + test1log,
" " + test2log if 2 in enabled else "",
" " + test3log if 2 in enabled else "",
"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
" ^\s*error <F-ERRCODE>401|403</F-ERRCODE> from <HOST>" \
r"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>",
r" ^\s*error <F-ERRCODE>401|403</F-ERRCODE> from <HOST>" \
if 2 in enabled else "",
"enabled = true" if 1 in enabled else "",
"",
@ -988,6 +1032,8 @@ class Fail2banServerTest(Fail2banClientServerBase):
"[test-jail2] Found 192.0.2.3",
"[test-jail2] Ban 192.0.2.3",
all=True)
# if observer available wait for it becomes idle (write all tickets to db):
_observer_wait_idle()
# rotate logs:
_write_file(test1log, "w+")
@ -1031,6 +1077,17 @@ class Fail2banServerTest(Fail2banClientServerBase):
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22",
"stdout: '[test-jail2] test-action3: ++ ban 192.0.2.22 ", all=True, wait=MID_WAITTIME)
# get banned ips:
_observer_wait_idle()
self.pruneLog("[test-phase 2d.1]")
self.execCmd(SUCCESS, startparams, "get", "test-jail2", "banip", "\n")
self.assertLogged(
"192.0.2.4", "192.0.2.8", "192.0.2.21", "192.0.2.22", all=True, wait=MID_WAITTIME)
self.pruneLog("[test-phase 2d.2]")
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip")
self.assertLogged(
"192.0.2.1", "192.0.2.2", "192.0.2.3", "192.0.2.4", "192.0.2.8", all=True, wait=MID_WAITTIME)
# restart jail with unban all:
self.pruneLog("[test-phase 2e]")
self.execCmd(SUCCESS, startparams,
@ -1121,7 +1178,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
"--async", "unban", "192.0.2.5", "192.0.2.6")
self.assertLogged(
"192.0.2.5 is not banned",
"[test-jail1] Unban 192.0.2.6", all=True
"[test-jail1] Unban 192.0.2.6", all=True, wait=MID_WAITTIME
)
# reload all (one jail) with unban all:
@ -1194,6 +1251,14 @@ class Fail2banServerTest(Fail2banClientServerBase):
"Jail 'test-jail1' stopped",
"Jail 'test-jail1' started", all=True, wait=MID_WAITTIME)
# Coverage for pickle of IPAddr (as string):
self.pruneLog("[test-phase end-3]")
self.execCmd(SUCCESS, startparams,
"--async", "set", "test-jail1", "addignoreip", "192.0.2.1/32", "2001:DB8::1/96")
self.execCmd(SUCCESS, startparams,
"--async", "get", "test-jail1", "ignoreip")
self.assertLogged("192.0.2.1/32", "2001:DB8::1/96", all=True)
# test action.d/nginx-block-map.conf --
@unittest.F2B.skip_if_cfg_missing(action="nginx-block-map")
@with_foreground_server_thread(startextra={
@ -1215,7 +1280,7 @@ class Fail2banServerTest(Fail2banClientServerBase):
'failregex = ^ failure "<F-ID>[^"]+</F-ID>" - <ADDR>',
'maxretry = 1', # ban by first failure
'enabled = true',
)
)
})
def testServerActions_NginxBlockMap(self, tmp, startparams):
cfg = pjoin(tmp, "config")
@ -1275,6 +1340,236 @@ class Fail2banServerTest(Fail2banClientServerBase):
mp = _read_file(mpfn)
self.assertEqual(mp, '')
@unittest.F2B.skip_if_cfg_missing(filter="sendmail-auth")
@with_foreground_server_thread(startextra={
# create log-file (avoid "not found" errors):
'create_before_start': ('%(tmp)s/test.log',),
'use_stock': True,
# fail2ban.local:
'f2b_local': (
'[DEFAULT]',
'dbmaxmatches = 1'
),
# jail.local config:
'jails': (
# default:
'''test_action = dummy[actionstart_on_demand=1, init="start: %(__name__)s", target="%(tmp)s/test.txt",
actionban='<known/actionban>;
echo "<matches>"; printf "=====\\n%%b\\n=====\\n\\n" "<matches>" >> <target>']''',
# jail sendmail-auth:
'[sendmail-auth]',
'backend = polling',
'usedns = no',
'logpath = %(tmp)s/test.log',
'action = %(test_action)s',
'filter = sendmail-auth[logtype=short]',
'datepattern = ^Epoch',
'maxretry = 3',
'maxmatches = 2',
'enabled = true',
# jail sendmail-reject:
'[sendmail-reject]',
'backend = polling',
'usedns = no',
'logpath = %(tmp)s/test.log',
'action = %(test_action)s',
'filter = sendmail-reject[logtype=short]',
'datepattern = ^Epoch',
'maxretry = 3',
'enabled = true',
)
})
def testServerJails_Sendmail(self, tmp, startparams):
cfg = pjoin(tmp, "config")
lgfn = '%(tmp)s/test.log' % {'tmp': tmp}
tofn = '%(tmp)s/test.txt' % {'tmp': tmp}
smaut_msg = (
str(int(MyTime.time())) + ' smtp1 sm-mta[5133]: s1000000000001: [192.0.2.1]: possible SMTP attack: command=AUTH, count=1',
str(int(MyTime.time())) + ' smtp1 sm-mta[5133]: s1000000000002: [192.0.2.1]: possible SMTP attack: command=AUTH, count=2',
str(int(MyTime.time())) + ' smtp1 sm-mta[5133]: s1000000000003: [192.0.2.1]: possible SMTP attack: command=AUTH, count=3',
)
smrej_msg = (
str(int(MyTime.time())) + ' smtp1 sm-mta[21134]: s2000000000001: ruleset=check_rcpt, arg1=<123@example.com>, relay=xxx.dynamic.example.com [192.0.2.2], reject=550 5.7.1 <123@example.com>... Relaying denied. Proper authentication required.',
str(int(MyTime.time())) + ' smtp1 sm-mta[21134]: s2000000000002: ruleset=check_rcpt, arg1=<345@example.com>, relay=xxx.dynamic.example.com [192.0.2.2], reject=550 5.7.1 <345@example.com>... Relaying denied. Proper authentication required.',
str(int(MyTime.time())) + ' smtp1 sm-mta[21134]: s3000000000003: ruleset=check_rcpt, arg1=<567@example.com>, relay=xxx.dynamic.example.com [192.0.2.2], reject=550 5.7.1 <567@example.com>... Relaying denied. Proper authentication required.',
)
self.pruneLog("[test-phase sendmail-auth]")
# write log:
_write_file(lgfn, "w+", *smaut_msg)
# wait and check it caused banned (and dump in the test-file):
self.assertLogged(
"[sendmail-auth] Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
_out_file(tofn)
td = _read_file(tofn)
# check matches (maxmatches = 2, so only 2 & 3 available):
m = smaut_msg[0]
self.assertNotIn(m, td)
for m in smaut_msg[1:]:
self.assertIn(m, td)
self.pruneLog("[test-phase sendmail-reject]")
# write log:
_write_file(lgfn, "w+", *smrej_msg)
# wait and check it caused banned (and dump in the test-file):
self.assertLogged(
"[sendmail-reject] Ban 192.0.2.2", "1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
_out_file(tofn)
td = _read_file(tofn)
# check matches (no maxmatches, so all matched messages are available):
for m in smrej_msg:
self.assertIn(m, td)
self.pruneLog("[test-phase restart sendmail-*]")
# restart jails (active ban-tickets should be restored):
self.execCmd(SUCCESS, startparams,
"reload", "--restart", "--all")
# wait a bit:
self.assertLogged(
"Reload finished.",
"[sendmail-auth] Restore Ban 192.0.2.1", "1 ticket(s) in 'sendmail-auth'", all=True, wait=MID_WAITTIME)
# check matches again - (dbmaxmatches = 1), so it should be only last match after restart:
td = _read_file(tofn)
m = smaut_msg[-1]
self.assertLogged(m)
self.assertIn(m, td)
for m in smaut_msg[0:-1]:
self.assertNotLogged(m)
self.assertNotIn(m, td)
# wait for restore of reject-jail:
self.assertLogged(
"[sendmail-reject] Restore Ban 192.0.2.2", "1 ticket(s) in 'sendmail-reject'", all=True, wait=MID_WAITTIME)
td = _read_file(tofn)
m = smrej_msg[-1]
self.assertLogged(m)
self.assertIn(m, td)
for m in smrej_msg[0:-1]:
self.assertNotLogged(m)
self.assertNotIn(m, td)
self.pruneLog("[test-phase stop server]")
# stop server and wait for end:
self.stopAndWaitForServerEnd(SUCCESS)
# just to debug actionstop:
self.assertFalse(exists(tofn))
@with_foreground_server_thread()
def testServerObserver(self, tmp, startparams):
cfg = pjoin(tmp, "config")
test1log = pjoin(tmp, "test1.log")
os.mkdir(pjoin(cfg, "action.d"))
def _write_action_cfg(actname="test-action1", prolong=True):
fn = pjoin(cfg, "action.d", "%s.conf" % actname)
_write_file(fn, "w",
"[DEFAULT]",
"",
"[Definition]",
"actionban = printf %%s \"[%(name)s] %(actname)s: ++ ban <ip> -c <bancount> -t <bantime> : <F-MSG>\"", \
"actionprolong = printf %%s \"[%(name)s] %(actname)s: ++ prolong <ip> -c <bancount> -t <bantime> : <F-MSG>\"" \
if prolong else "",
"actionunban = printf %%b '[%(name)s] %(actname)s: -- unban <ip>'",
)
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
_out_file(fn)
def _write_jail_cfg(backend="polling"):
_write_file(pjoin(cfg, "jail.conf"), "w",
"[INCLUDES]", "",
"[DEFAULT]", "",
"usedns = no",
"maxretry = 3",
"findtime = 1m",
"bantime = 5m",
"bantime.increment = true",
"datepattern = {^LN-BEG}EPOCH",
"",
"[test-jail1]", "backend = " + backend, "filter =",
"action = test-action1[name='%(__name__)s']",
" test-action2[name='%(__name__)s']",
"logpath = " + test1log,
r"failregex = ^\s*failure <F-ERRCODE>401|403</F-ERRCODE> from <HOST>:\s*<F-MSG>.*</F-MSG>$",
"enabled = true",
"",
)
if unittest.F2B.log_level <= logging.DEBUG: # pragma: no cover
_out_file(pjoin(cfg, "jail.conf"))
# create test config:
_write_action_cfg(actname="test-action1", prolong=False)
_write_action_cfg(actname="test-action2", prolong=True)
_write_jail_cfg()
_write_file(test1log, "w")
# initial start:
self.pruneLog("[test-phase 0) time-0]")
self.execCmd(SUCCESS, startparams, "reload")
# generate bad ip:
_write_file(test1log, "w+", *(
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm bad \"hacker\" `` $(echo test)",) * 3
))
# wait for ban:
_observer_wait_idle()
self.assertLogged(
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 1 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 1 -t 300 : ",
all=True, wait=MID_WAITTIME)
# wait for observer idle (write all tickets to db):
_observer_wait_idle()
self.pruneLog("[test-phase 1) time+10m]")
# jump to the future (+10 minutes):
_time_shift(10)
_observer_wait_idle()
self.assertLogged(
"stdout: '[test-jail1] test-action1: -- unban 192.0.2.11",
"stdout: '[test-jail1] test-action2: -- unban 192.0.2.11",
"0 ticket(s) in 'test-jail1'",
all=True, wait=MID_WAITTIME)
_observer_wait_idle()
self.pruneLog("[test-phase 2) time+10m]")
# following tests are time-related - observer can prolong ticket (increase ban-time)
# before banning, so block it here before banFound called, prolong case later:
wakeObs = False
_observer_wait_before_incrban(lambda: wakeObs)
# write again (IP already bad):
_write_file(test1log, "w+", *(
(str(int(MyTime.time())) + " failure 401 from 192.0.2.11: I'm very bad \"hacker\" `` $(echo test)",) * 2
))
# wait for ban:
self.assertLogged(
"stdout: '[test-jail1] test-action1: ++ ban 192.0.2.11 -c 2 -t 300 : ",
"stdout: '[test-jail1] test-action2: ++ ban 192.0.2.11 -c 2 -t 300 : ",
all=True, wait=MID_WAITTIME)
# get banned ips with time:
self.pruneLog("[test-phase 2) time+10m - get-ips]")
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time")
self.assertLogged(
"192.0.2.11", "+ 300 =", all=True, wait=MID_WAITTIME)
# unblock observer here and wait it is done:
wakeObs = True
_observer_wait_idle()
self.pruneLog("[test-phase 2) time+11m]")
# jump to the future (+1 minute):
_time_shift(1)
# wait for observer idle (write all tickets to db):
_observer_wait_idle()
# wait for prolong:
self.assertLogged(
"stdout: '[test-jail1] test-action2: ++ prolong 192.0.2.11 -c 2 -t 600 : ",
all=True, wait=MID_WAITTIME)
# get banned ips with time:
_observer_wait_idle()
self.pruneLog("[test-phase 2) time+11m - get-ips]")
self.execCmd(SUCCESS, startparams, "get", "test-jail1", "banip", "--with-time")
self.assertLogged(
"192.0.2.11", "+ 600 =", all=True, wait=MID_WAITTIME)
# test multiple start/stop of the server (threaded in foreground) --
if False: # pragma: no cover
@with_foreground_server_thread()
@ -1285,3 +1580,4 @@ class Fail2banServerTest(Fail2banClientServerBase):
def testServerStartStop(self):
for i in xrange(2000):
self._testServerStartStop()

View file

@ -25,6 +25,7 @@ __license__ = "GPL"
import os
import sys
import unittest
from ..client import fail2banregex
from ..client.fail2banregex import Fail2banRegex, get_opt_parser, exec_command_line, output, str2LogLevel
@ -51,6 +52,10 @@ def _Fail2banRegex(*args):
logSys.setLevel(str2LogLevel(opts.log_level))
return (opts, args, Fail2banRegex(opts))
def _test_exec(*args):
(opts, args, fail2banRegex) = _Fail2banRegex(*args)
return fail2banRegex.start(args)
class ExitException(Exception):
def __init__(self, code):
self.code = code
@ -75,23 +80,27 @@ def _test_exec_command_line(*args):
sys.stderr = _org['stderr']
return _exit_code
STR_00 = "Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0"
RE_00 = r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>"
RE_00_ID = r"Authentication failure for <F-ID>.*?</F-ID> from <HOST>$"
RE_00_USER = r"Authentication failure for <F-USER>.*?</F-USER> from <HOST>$"
FILENAME_01 = os.path.join(TEST_FILES_DIR, "testcase01.log")
FILENAME_02 = os.path.join(TEST_FILES_DIR, "testcase02.log")
FILENAME_WRONGCHAR = os.path.join(TEST_FILES_DIR, "testcase-wrong-char.log")
FILENAME_SSHD = os.path.join(TEST_FILES_DIR, "logs", "sshd")
FILTER_SSHD = os.path.join(CONFIG_DIR, 'filter.d', 'sshd.conf')
FILENAME_ZZZ_SSHD = os.path.join(TEST_FILES_DIR, 'zzz-sshd-obsolete-multiline.log')
FILTER_ZZZ_SSHD = os.path.join(TEST_CONFIG_DIR, 'filter.d', 'zzz-sshd-obsolete-multiline.conf')
FILENAME_ZZZ_GEN = os.path.join(TEST_FILES_DIR, "logs", "zzz-generic-example")
FILTER_ZZZ_GEN = os.path.join(TEST_CONFIG_DIR, 'filter.d', 'zzz-generic-example.conf')
class Fail2banRegexTest(LogCaptureTestCase):
RE_00 = r"(?:(?:Authentication failure|Failed [-/\w+]+) for(?: [iI](?:llegal|nvalid) user)?|[Ii](?:llegal|nvalid) user|ROOT LOGIN REFUSED) .*(?: from|FROM) <HOST>"
FILENAME_01 = os.path.join(TEST_FILES_DIR, "testcase01.log")
FILENAME_02 = os.path.join(TEST_FILES_DIR, "testcase02.log")
FILENAME_WRONGCHAR = os.path.join(TEST_FILES_DIR, "testcase-wrong-char.log")
FILENAME_SSHD = os.path.join(TEST_FILES_DIR, "logs", "sshd")
FILTER_SSHD = os.path.join(CONFIG_DIR, 'filter.d', 'sshd.conf')
FILENAME_ZZZ_SSHD = os.path.join(TEST_FILES_DIR, 'zzz-sshd-obsolete-multiline.log')
FILTER_ZZZ_SSHD = os.path.join(TEST_CONFIG_DIR, 'filter.d', 'zzz-sshd-obsolete-multiline.conf')
FILENAME_ZZZ_GEN = os.path.join(TEST_FILES_DIR, "logs", "zzz-generic-example")
FILTER_ZZZ_GEN = os.path.join(TEST_CONFIG_DIR, 'filter.d', 'zzz-generic-example.conf')
def setUp(self):
"""Call before every test case."""
LogCaptureTestCase.setUp(self)
@ -103,57 +112,50 @@ class Fail2banRegexTest(LogCaptureTestCase):
tearDownMyTime()
def testWrongRE(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertFalse(_test_exec(
"test", r".** from <HOST>$"
)
self.assertFalse(fail2banRegex.start(args))
))
self.assertLogged("Unable to compile regular expression")
def testWrongIngnoreRE(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertFalse(_test_exec(
"--datepattern", "{^LN-BEG}EPOCH",
"test", r".*? from <HOST>$", r".**"
)
self.assertFalse(fail2banRegex.start(args))
))
self.assertLogged("Unable to compile regular expression")
def testDirectFound(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--print-all-matched", "--print-no-missed",
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
STR_00,
r"Authentication failure for .*? from <HOST>$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed')
def testDirectNotFound(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"--print-all-missed",
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
STR_00,
r"XYZ from <HOST>$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 1 lines, 0 ignored, 0 matched, 1 missed')
def testDirectIgnored(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"--print-all-ignored",
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
STR_00,
r"Authentication failure for .*? from <HOST>$",
r"kevin from 192.0.2.0$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 1 lines, 1 ignored, 0 matched, 0 missed')
def testDirectRE_1(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--print-all-matched",
Fail2banRegexTest.FILENAME_01,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_01, RE_00
))
self.assertLogged('Lines: 19 lines, 0 ignored, 13 matched, 6 missed')
self.assertLogged('Error decoding line');
@ -163,69 +165,78 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.assertLogged('Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 87.142.124.10')
def testDirectRE_1raw(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--print-all-matched", "--raw",
Fail2banRegexTest.FILENAME_01,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_01, RE_00
))
self.assertLogged('Lines: 19 lines, 0 ignored, 16 matched, 3 missed')
def testDirectRE_1raw_noDns(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--print-all-matched", "--raw", "--usedns=no",
Fail2banRegexTest.FILENAME_01,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_01, RE_00
))
self.assertLogged('Lines: 19 lines, 0 ignored, 13 matched, 6 missed')
# usage of <F-ID>\S+</F-ID> causes raw handling automatically:
self.pruneLog()
self.assertTrue(_test_exec(
"-d", "^Epoch",
"1490349000 test failed.dns.ch", "^\s*test <F-ID>\S+</F-ID>"
))
self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed', all=True)
self.assertNotLogged('Unable to find a corresponding IP address')
def testDirectRE_2(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--print-all-matched",
Fail2banRegexTest.FILENAME_02,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_02, RE_00
))
self.assertLogged('Lines: 13 lines, 0 ignored, 5 matched, 8 missed')
def testVerbose(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--timezone", "UTC+0200",
"--verbose", "--verbose-date", "--print-no-missed",
Fail2banRegexTest.FILENAME_02,
Fail2banRegexTest.RE_00
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_02, RE_00
))
self.assertLogged('Lines: 13 lines, 0 ignored, 5 matched, 8 missed')
self.assertLogged('141.3.81.106 Sun Aug 14 11:53:59 2005')
self.assertLogged('141.3.81.106 Sun Aug 14 11:54:59 2005')
def testVerboseFullSshd(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"-v", "--verbose-date", "--print-all-matched", "--print-all-ignored",
"-c", CONFIG_DIR,
Fail2banRegexTest.FILENAME_SSHD, "sshd"
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_SSHD, "sshd"
))
# test failure line and not-failure lines both presents:
self.assertLogged("[29116]: User root not allowed because account is locked",
"[29116]: Received disconnect from 1.2.3.4", all=True)
self.pruneLog()
# show real options:
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"-vv", "-c", CONFIG_DIR,
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.1",
"sshd[logtype=short]"
))
# tet logtype is specified and set in real options:
self.assertLogged("Real filter options :", "'logtype': 'short'", all=True)
self.assertNotLogged("'logtype': 'file'", "'logtype': 'journal'", all=True)
def testFastSshd(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"--print-all-matched",
"-c", CONFIG_DIR,
Fail2banRegexTest.FILENAME_ZZZ_SSHD, "sshd.conf[mode=normal]"
)
self.assertTrue(fail2banRegex.start(args))
FILENAME_ZZZ_SSHD, "sshd.conf[mode=normal]"
))
# test failure line and all not-failure lines presents:
self.assertLogged(
"[29116]: Connection from 192.0.2.4",
@ -234,80 +245,107 @@ class Fail2banRegexTest(LogCaptureTestCase):
def testMultilineSshd(self):
# by the way test of missing lines by multiline in `for bufLine in orgLineBuffer[int(fullBuffer):]`
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"--print-all-matched", "--print-all-missed",
"-c", os.path.dirname(Fail2banRegexTest.FILTER_ZZZ_SSHD),
Fail2banRegexTest.FILENAME_ZZZ_SSHD, os.path.basename(Fail2banRegexTest.FILTER_ZZZ_SSHD)
)
self.assertTrue(fail2banRegex.start(args))
"-c", os.path.dirname(FILTER_ZZZ_SSHD),
FILENAME_ZZZ_SSHD, os.path.basename(FILTER_ZZZ_SSHD)
))
# test "failure" line presents (2nd part only, because multiline fewer precise):
self.assertLogged(
"[29116]: Received disconnect from 192.0.2.4", all=True)
def testFullGeneric(self):
# by the way test of ignoreregex (specified in filter file)...
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
Fail2banRegexTest.FILENAME_ZZZ_GEN, Fail2banRegexTest.FILTER_ZZZ_GEN+"[mode=test]"
)
self.assertTrue(fail2banRegex.start(args))
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
FILENAME_ZZZ_GEN, FILTER_ZZZ_GEN+"[mode=test]"
))
def testDirectMultilineBuf(self):
# test it with some pre-lines also to cover correct buffer scrolling (all multi-lines printed):
for preLines in (0, 20):
self.pruneLog("[test-phase %s]" % preLines)
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"--usedns", "no", "-d", "^Epoch", "--print-all-matched", "--maxlines", "5",
("1490349000 TEST-NL\n"*preLines) +
"1490349000 FAIL\n1490349000 TEST1\n1490349001 TEST2\n1490349001 HOST 192.0.2.34",
r"^\s*FAIL\s*$<SKIPLINES>^\s*HOST <HOST>\s*$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: %s lines, 0 ignored, 2 matched, %s missed' % (preLines+4, preLines+2))
# both matched lines were printed:
self.assertLogged("| 1490349000 FAIL", "| 1490349001 HOST 192.0.2.34", all=True)
def testDirectMultilineBufDebuggex(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"--usedns", "no", "-d", "^Epoch", "--debuggex", "--print-all-matched", "--maxlines", "5",
"1490349000 FAIL\n1490349000 TEST1\n1490349001 TEST2\n1490349001 HOST 192.0.2.34",
r"^\s*FAIL\s*$<SKIPLINES>^\s*HOST <HOST>\s*$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 4 lines, 0 ignored, 2 matched, 2 missed')
# the sequence in args-dict is currently undefined (so can be 1st argument)
self.assertLogged("&flags=m", "?flags=m")
def testSinglelineWithNLinContent(self):
#
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"--usedns", "no", "-d", "^Epoch", "--print-all-matched",
"1490349000 FAIL: failure\nhost: 192.0.2.35",
r"^\s*FAIL:\s*.*\nhost:\s+<HOST>$"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed')
def testRegexEpochPatterns(self):
(opts, args, fail2banRegex) = _Fail2banRegex(
self.assertTrue(_test_exec(
"-r", "-d", r"^\[{LEPOCH}\]\s+", "--maxlines", "5",
"[1516469849] 192.0.2.1 FAIL: failure\n"
"[1516469849551] 192.0.2.2 FAIL: failure\n"
"[1516469849551000] 192.0.2.3 FAIL: failure\n"
"[1516469849551.000] 192.0.2.4 FAIL: failure",
r"^<HOST> FAIL\b"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Lines: 4 lines, 0 ignored, 4 matched, 0 missed')
def testRegexSubnet(self):
self.assertTrue(_test_exec(
"-vv", "-d", r"^\[{LEPOCH}\]\s+", "--maxlines", "5",
"[1516469849] 192.0.2.1 FAIL: failure\n"
"[1516469849] 192.0.2.1/24 FAIL: failure\n"
"[1516469849] 2001:DB8:FF:FF::1 FAIL: failure\n"
"[1516469849] 2001:DB8:FF:FF::1/60 FAIL: failure\n",
r"^<SUBNET> FAIL\b"
))
self.assertLogged('Lines: 4 lines, 0 ignored, 4 matched, 0 missed')
self.assertLogged('192.0.2.0/24', '2001:db8:ff:f0::/60', all=True)
def testFrmtOutput(self):
# id/ip only:
self.assertTrue(_test_exec('-o', 'id', STR_00, RE_00_ID))
self.assertLogged('kevin')
self.pruneLog()
# row with id :
self.assertTrue(_test_exec('-o', 'row', STR_00, RE_00_ID))
self.assertLogged("['kevin'", "'ip4': '192.0.2.0'", "'fid': 'kevin'", all=True)
self.pruneLog()
# row with ip :
self.assertTrue(_test_exec('-o', 'row', STR_00, RE_00_USER))
self.assertLogged("['192.0.2.0'", "'ip4': '192.0.2.0'", "'user': 'kevin'", all=True)
self.pruneLog()
# log msg :
self.assertTrue(_test_exec('-o', 'msg', STR_00, RE_00_USER))
self.assertLogged(STR_00)
self.pruneLog()
# item of match (user):
self.assertTrue(_test_exec('-o', 'user', STR_00, RE_00_USER))
self.assertLogged('kevin')
self.pruneLog()
def testWrongFilterFile(self):
# use test log as filter file to cover eror cases...
(opts, args, fail2banRegex) = _Fail2banRegex(
Fail2banRegexTest.FILENAME_ZZZ_GEN, Fail2banRegexTest.FILENAME_ZZZ_GEN
)
self.assertFalse(fail2banRegex.start(args))
self.assertFalse(_test_exec(
FILENAME_ZZZ_GEN, FILENAME_ZZZ_GEN
))
def _reset(self):
# reset global warn-counter:
@ -315,13 +353,13 @@ class Fail2banRegexTest(LogCaptureTestCase):
_decode_line_warn.clear()
def testWronChar(self):
unittest.F2B.SkipIfCfgMissing(stock=True)
self._reset()
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
Fail2banRegexTest.FILENAME_WRONGCHAR, Fail2banRegexTest.FILTER_SSHD
)
self.assertTrue(fail2banRegex.start(args))
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
FILENAME_WRONGCHAR, FILTER_SSHD
))
self.assertLogged('Lines: 4 lines, 0 ignored, 2 matched, 2 missed')
self.assertLogged('Error decoding line')
@ -331,15 +369,15 @@ class Fail2banRegexTest(LogCaptureTestCase):
self.assertLogged('Nov 8 00:16:12 main sshd[32547]: pam_succeed_if(sshd:auth): error retrieving information about user llinco')
def testWronCharDebuggex(self):
unittest.F2B.SkipIfCfgMissing(stock=True)
self._reset()
(opts, args, fail2banRegex) = _Fail2banRegex(
"-l", "notice", # put down log-level, because of too many debug-messages
"--datepattern", "^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
self.assertTrue(_test_exec(
"-l", "notice", # put down log-level, because of too many debug-messages
"--datepattern", r"^(?:%a )?%b %d %H:%M:%S(?:\.%f)?(?: %ExY)?",
"--debuggex", "--print-all-matched",
Fail2banRegexTest.FILENAME_WRONGCHAR, Fail2banRegexTest.FILTER_SSHD,
FILENAME_WRONGCHAR, FILTER_SSHD,
r"llinco[^\\]"
)
self.assertTrue(fail2banRegex.start(args))
))
self.assertLogged('Error decoding line')
self.assertLogged('Lines: 4 lines, 1 ignored, 2 matched, 1 missed')
@ -356,15 +394,48 @@ class Fail2banRegexTest(LogCaptureTestCase):
def testExecCmdLine_Direct(self):
self.assertEqual(_test_exec_command_line(
'-l', 'info',
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
r"Authentication failure for .*? from <HOST>$"
STR_00, r"Authentication failure for .*? from <HOST>$"
), 0)
self.assertLogged('Lines: 1 lines, 0 ignored, 1 matched, 0 missed')
def testExecCmdLine_MissFailID(self):
self.assertNotEqual(_test_exec_command_line(
'-l', 'info',
"Dec 31 11:59:59 [sshd] error: PAM: Authentication failure for kevin from 192.0.2.0",
r"Authentication failure"
STR_00, r"Authentication failure"
), 0)
self.assertLogged('No failure-id group in ')
def testExecCmdLine_ErrorParam(self):
# single line error:
self.assertNotEqual(_test_exec_command_line(
'-l', 'notice', '-d', '%:%.%-', 'LOG', 'RE'
), 0)
self.assertLogged('ERROR: Failed to set datepattern')
# verbose (traceback/callstack):
self.pruneLog()
self.assertNotEqual(_test_exec_command_line(
'-v', '-d', '%:%.%-', 'LOG', 'RE'
), 0)
self.assertLogged('Failed to set datepattern')
def testLogtypeSystemdJournal(self): # pragma: no cover
if not fail2banregex.FilterSystemd:
raise unittest.SkipTest('Skip test because no systemd backand available')
self.assertTrue(_test_exec(
"systemd-journal", FILTER_ZZZ_GEN
+'[journalmatch="SYSLOG_IDENTIFIER=\x01\x02dummy\x02\x01",'
+' failregex="^\x00\x01\x02dummy regex, never match <F-ID>xxx</F-ID>"]'
))
self.assertLogged("'logtype': 'journal'")
self.assertNotLogged("'logtype': 'file'")
self.assertLogged('Lines: 0 lines, 0 ignored, 0 matched, 0 missed')
self.pruneLog()
# logtype specified explicitly (should win in filter):
self.assertTrue(_test_exec(
"systemd-journal", FILTER_ZZZ_GEN
+'[logtype=file,'
+' journalmatch="SYSLOG_IDENTIFIER=\x01\x02dummy\x02\x01",'
+' failregex="^\x00\x01\x02dummy regex, never match <F-ID>xxx</F-ID>"]'
))
self.assertLogged("'logtype': 'file'")
self.assertNotLogged("'logtype': 'journal'")

View file

@ -69,9 +69,9 @@ class AddFailure(unittest.TestCase):
self.assertEqual(self.__failManager.getFailTotal(), 0)
self.__failManager.setFailTotal(13)
def testFailManagerAdd_MaxEntries(self):
maxEntries = 2
self.__failManager.maxEntries = maxEntries
def testFailManagerAdd_MaxMatches(self):
maxMatches = 2
self.__failManager.maxMatches = maxMatches
failures = ["abc\n", "123\n", "ABC\n", "1234\n"]
# add failures sequential:
i = 80
@ -86,8 +86,8 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# should retrieve 2 matches only, but count of all attempts (4):
self.assertEqual(ticket.getAttempt(), len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# add more failures at once:
ticket = FailTicket("127.0.0.1", 1000002000 - 10, failures)
ticket.setAttempt(len(failures))
@ -98,8 +98,8 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# should retrieve 2 matches only, but count of all attempts (8):
self.assertEqual(ticket.getAttempt(), 2 * len(failures))
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# add self ticket again:
self.__failManager.addFailure(ticket)
#
@ -108,8 +108,16 @@ class AddFailure(unittest.TestCase):
ticket = manFailList["127.0.0.1"]
# same matches, but +1 attempt (9)
self.assertEqual(ticket.getAttempt(), 2 * len(failures) + 1)
self.assertEqual(len(ticket.getMatches()), maxEntries)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxEntries:])
self.assertEqual(len(ticket.getMatches()), maxMatches)
self.assertEqual(ticket.getMatches(), failures[len(failures) - maxMatches:])
# no matches by maxMatches == 0 :
self.__failManager.maxMatches = 0
self.__failManager.addFailure(ticket)
manFailList = self.__failManager._FailManager__failList
ticket = manFailList["127.0.0.1"]
self.assertEqual(len(ticket.getMatches()), 0)
# test set matches None to None:
ticket.setMatches(None)
def testFailManagerMaxTime(self):
self._addDefItems()
@ -151,10 +159,10 @@ class AddFailure(unittest.TestCase):
ticket_repr = repr(ticket)
self.assertEqual(
ticket_str,
'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]')
'FailTicket: ip=193.168.0.128 time=1167605999.0 bantime=None bancount=0 #attempts=5 matches=[]')
self.assertEqual(
ticket_repr,
'FailTicket: ip=193.168.0.128 time=1167605999.0 #attempts=5 matches=[]')
'FailTicket: ip=193.168.0.128 time=1167605999.0 bantime=None bancount=0 #attempts=5 matches=[]')
self.assertFalse(not ticket)
# and some get/set-ers otherwise not tested
ticket.setTime(1000002000.0)
@ -162,7 +170,7 @@ class AddFailure(unittest.TestCase):
# and str() adjusted correspondingly
self.assertEqual(
str(ticket),
'FailTicket: ip=193.168.0.128 time=1000002000.0 #attempts=5 matches=[]')
'FailTicket: ip=193.168.0.128 time=1000002000.0 bantime=None bancount=0 #attempts=5 matches=[]')
def testbanNOK(self):
self._addDefItems()

View file

@ -12,4 +12,9 @@ class TestAction(ActionBase):
del aInfo['ip']
self._logSys.info("%s unban deleted aInfo IP", self._name)
def flush(self):
# intended error to cover no unhandled exception occurs in flush
# as well as unbans are done individually after errored flush.
raise ValueError("intended error")
Action = TestAction

Binary file not shown.

Binary file not shown.

View file

@ -134,6 +134,14 @@
# failJSON: { "time": "2018-03-28T01:31:42", "match": true , "host": "91.49.82.139" }
[Wed Mar 28 01:31:42.355210 2018] [ssl:error] [pid 6586] [client 91.49.82.139:58028] AH02033: No hostname was provided via SNI for a name based virtual host
# failJSON: { "match": false, "desc": "ignore mod_evasive errors in normal mode (gh-2548)" }
[Thu Oct 17 18:43:40.160521 2019] [evasive20:error] [pid 22589] [client 192.0.2.1:56175] client denied by server configuration: /path/index.php, referer: https://hostname/path/
# filterOptions: {"mode": "aggressive"}
# failJSON: { "time": "2019-10-17T18:43:40", "match": true, "host": "192.0.2.1", "desc": "accept mod_evasive errors in aggressive mode (gh-2548)" }
[Thu Oct 17 18:43:40.160521 2019] [evasive20:error] [pid 22589] [client 192.0.2.1:56175] client denied by server configuration: /path/index.php, referer: https://hostname/path/
# filterOptions: {"logging": "syslog"}
# failJSON: { "time": "2005-02-15T16:23:00", "match": true , "host": "192.0.2.1", "desc": "using syslog (ErrorLog syslog)" }

View file

@ -3,3 +3,6 @@
# failJSON: { "time": "2013-12-28T09:18:05", "match": true , "host": "32.65.254.69", "desc": "additional entry (and exact one space)" }
[Sat Dec 28 09:18:05 2013] [error] [client 32.65.254.69] ModSecurity: [file "/etc/httpd/modsecurity.d/10_asl_rules.conf"] [line "635"] [id "340069"] [rev "4"] [msg "Atomicorp.com UNSUPPORTED DELAYED Rules: Web vulnerability scanner"] [severity "CRITICAL"] Access denied with code 403 (phase 2). Pattern match "(?:nessus(?:_is_probing_you_|test)|^/w00tw00t\\\\.at\\\\.)" at REQUEST_URI. [hostname "192.81.249.191"] [uri "/w00tw00t.at.blackhats.romanian.anti-sec:)"] [unique_id "4Q6RdsBR@b4AAA65LRUAAAAA"]
# failJSON: { "time": "2018-09-28T09:18:06", "match": true , "host": "192.0.2.1", "desc": "two client entries in message (gh-2247)" }
[Sat Sep 28 09:18:06 2018] [error] [client 192.0.2.1:55555] [client 192.0.2.1] ModSecurity: [file "/etc/httpd/modsecurity.d/10_asl_rules.conf"] [line "635"] [id "340069"] [rev "4"] [msg "Atomicorp.com UNSUPPORTED DELAYED Rules: Web vulnerability scanner"] [severity "CRITICAL"] Access denied with code 403 (phase 2). Pattern match "(?:nessus(?:_is_probing_you_|test)|^/w00tw00t\\\\.at\\\\.)" at REQUEST_URI. [hostname "192.81.249.191"] [uri "/w00tw00t.at.blackhats.romanian.anti-sec:)"] [unique_id "4Q6RdsBR@b4AAA65LRUAAAAA"]

Some files were not shown because too many files have changed in this diff Show more