mirror of
https://github.com/fail2ban/fail2ban.git
synced 2026-03-11 08:55:31 +00:00
New upstream version 0.11.1
This commit is contained in:
parent
dcb9340599
commit
ad3e0d97c4
134 changed files with 5907 additions and 1373 deletions
63
.travis.yml
63
.travis.yml
|
|
@ -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
192
ChangeLog
|
|
@ -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:
|
||||
|
|
|
|||
15
MANIFEST
15
MANIFEST
|
|
@ -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
|
||||
|
|
|
|||
28
README.md
28
README.md
|
|
@ -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:
|
||||
------------
|
||||
|
||||
* [](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) travis-ci.org (0.10 branch) / [](https://travis-ci.org/fail2ban/fail2ban) travis-ci.org (master branch)
|
||||
* travis-ci.org: [](https://travis-ci.org/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) (0.10 branch)
|
||||
|
||||
* [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10)
|
||||
* coveralls.io: [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10) / (0.10 branch)
|
||||
|
||||
* [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10)
|
||||
* codecov.io: [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.11) (0.11 branch) / [](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10) (0.10 branch)
|
||||
|
||||
Contact:
|
||||
--------
|
||||
|
|
|
|||
2
THANKS
2
THANKS
|
|
@ -111,7 +111,7 @@ Russell Odom
|
|||
SATO Kentaro
|
||||
Sean DuBois
|
||||
Sebastian Arcus
|
||||
Serg G. Brester
|
||||
Serg G. Brester (sebres)
|
||||
Sergey Safarov
|
||||
Shaun C.
|
||||
Sireyessire
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
203
config/action.d/nftables.conf
Normal file
203
config/action.d/nftables.conf
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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): )?
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
6
config/filter.d/bitwarden.conf
Normal file
6
config/filter.d/bitwarden.conf
Normal 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>$
|
||||
9
config/filter.d/centreon.conf
Normal file
9
config/filter.d/centreon.conf
Normal 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>'
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
56
config/filter.d/traefik-auth.conf
Normal file
56
config/filter.d/traefik-auth.conf
Normal 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 =
|
||||
34
config/filter.d/znc-adminlog.conf
Normal file
34
config/filter.d/znc-adminlog.conf
Normal 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/)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",""],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
##
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
529
fail2ban/server/observer.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ;) --
|
||||
|
|
|
|||
|
|
@ -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$
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
BIN
fail2ban/tests/files/database_v2.db
Normal file
BIN
fail2ban/tests/files/database_v2.db
Normal file
Binary file not shown.
|
|
@ -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)" }
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue