Merge branch 'upstream'

debian
Sylvestre Ledru 2022-09-28 07:25:24 -10:00
commit 749e5b4694
154 changed files with 3390 additions and 2195 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
ChangeLog linguist-language=Markdown

4
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [sebres]
custom: [paypal.me/sebres]

View File

@ -1,49 +0,0 @@
_We will be very grateful, if your problem was described as completely as possible,
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
within INFO mode), and configuration in particular of effected relevant settings
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
jail troubleshooting).
Thank you in advance for the details, because such issues like "It does not work"
alone could not help to resolve anything!
Thanks! (remove this paragraph and other comments upon reading)_
### Environment:
_Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
and you can't verify that the issue persists in the recent release, better seek support
from the distribution you obtained Fail2Ban from_
- Fail2Ban version (including any possible distribution suffixes):
- OS, including release name/version:
- [ ] Fail2Ban installed via OS/distribution mechanisms
- [ ] You have not applied any additional foreign patches to the codebase
- [ ] Some customizations were done to the configuration (provide details below is so)
### The issue:
_Summary here_
#### Steps to reproduce
#### Expected behavior
#### Observed behavior
#### Any additional information
### Configuration, dump and another helpful excerpts
#### Any customizations done to /etc/fail2ban/ configuration
```
```
#### Relevant parts of /var/log/fail2ban.log file:
_preferably obtained while running fail2ban with `loglevel = 4`_
```
```
#### Relevant lines from monitored log files in question:
```
```

70
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,70 @@
---
name: Bug report
about: Report a bug within the fail2ban engines (not filters or jails)
title: '[BR]: '
labels: bug
assignees: ''
---
<!--
- Before reporting, please make sure to search the open and closed issues for any reports in the past.
- Use this issue template to report a bug in the fail2ban engine (not in a filter or jail).
- If you want to request a feature or a new filter, please use "Feature request" or "Filter request" instead.
- If you have rather some question, please open or join to some discussion.
We will be very grateful, if your problem was described as completely as possible,
enclosing excerpts from logs (if possible within DEBUG mode, if no errors evident
within INFO mode), and configuration in particular of effected relevant settings
(e.g., with ` fail2ban-client -d | grep 'affected-jail-name' ` for a particular
jail troubleshooting).
Thank you in advance for the details, because such issues like "It does not work"
alone could not help to resolve anything!
Thanks!
(you can remove this paragraph and other comments upon reading)
-->
### Environment:
<!--
Fill out and check (`[x]`) the boxes which apply. If your Fail2Ban version is outdated,
and you can't verify that the issue persists in the recent release, better seek support
from the distribution you obtained Fail2Ban from
-->
- Fail2Ban version <!-- including any possible distribution suffixes --> :
- OS, including release name/version :
- [ ] Fail2Ban installed via OS/distribution mechanisms
- [ ] You have not applied any additional foreign patches to the codebase
- [ ] Some customizations were done to the configuration (provide details below is so)
### The issue:
<!-- summary here -->
#### Steps to reproduce
#### Expected behavior
#### Observed behavior
#### Any additional information
### Configuration, dump and another helpful excerpts
#### Any customizations done to /etc/fail2ban/ configuration
<!-- put your configuration excerpts between next 2 lines -->
```
```
#### Relevant parts of /var/log/fail2ban.log file:
<!-- preferably obtained while running fail2ban with `loglevel = 4` -->
<!-- put your log excerpt between next 2 lines -->
```
```
#### Relevant lines from monitored log files:
<!-- put your log excerpt between next 2 lines -->
```
```

View File

@ -0,0 +1,35 @@
---
name: Feature request
about: Suggest an idea or an enhancement for this project
title: '[RFE]: '
labels: enhancement
assignees: ''
---
<!--
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
- Use this issue template to request a feature in the fail2ban engine (not a new filter or jail).
- If you want to request a new filter or failregex, please use "Filter request" instead.
- If you have rather some question, please open or join to some discussion.
-->
#### Feature request type
<!--
Please provide a summary description of the feature request.
-->
#### Description
<!--
Please describe the feature in more detail.
-->
#### Considered alternatives
<!--
A clear and concise description of any alternative solutions or features you've considered.
-->
#### Any additional information
<!--
Add any other context or screenshots about the feature request here.
-->

View File

@ -0,0 +1,59 @@
---
name: Filter request
about: Request a new jail or filter to be supported or existing filter extended with new failregex
title: '[FR]: '
labels: filter-request
assignees: ''
---
<!--
- Before requesting, please make sure to search the open and closed issues for any requests in the past.
- Sometimes failregex have been already requested before but are not implemented yet due to various reasons.
- If there are no hits for your concerns, please proceed otherwise add a comment to the related issue (also if it is closed).
- If you want to request a new feature, please use "Feature request" instead.
- If you have rather some question, please open or join to some discussion.
-->
### Environment:
<!--
Fill out and check (`[x]`) the boxes which apply.
-->
- Fail2Ban version <!-- including any possible distribution suffixes --> :
- OS, including release name/version :
#### Service, project or product which log or journal should be monitored
- Name of filter or jail in Fail2Ban (if already exists) :
- Service, project or product name, including release name/version :
- Repository or URL (if known) :
- Service type :
- Ports and protocols the service is listening :
#### Log or journal information
<!-- Delete unrelated group -->
<!-- Log file -->
- Log file name(s) :
<!-- Systemd journal -->
- Journal identifier or unit name :
#### Any additional information
### Relevant lines from monitored log files:
#### failures in sense of fail2ban filter (fail2ban must match):
<!-- put your log excerpt between next 2 lines -->
```
```
#### legitimate messages (fail2ban should not consider as failures):
<!-- put your log excerpt between next 2 lines -->
```
```

View File

@ -1,7 +1,8 @@
Before submitting your PR, please review the following checklist: Before submitting your PR, please review the following checklist:
- [ ] **CHOOSE CORRECT BRANCH**: if filing a bugfix/enhancement - [ ] **CHOOSE CORRECT BRANCH**: if filing a bugfix/enhancement
against 0.9.x series, choose `master` branch against certain release version, choose `0.9`, `0.10` or `0.11` branch,
for dev-edition use `master` branch
- [ ] **CONSIDER adding a unit test** if your PR resolves an issue - [ ] **CONSIDER adding a unit test** if your PR resolves an issue
- [ ] **LIST ISSUES** this PR resolves - [ ] **LIST ISSUES** this PR resolves
- [ ] **MAKE SURE** this PR doesn't break existing tests - [ ] **MAKE SURE** this PR doesn't break existing tests

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy: strategy:
matrix: matrix:
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3] python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, '3.10', '3.11.0-beta.3', pypy2, pypy3]
fail-fast: false fail-fast: false
# Steps represent a sequence of tasks that will be executed as part of the job # Steps represent a sequence of tasks that will be executed as part of the job
steps: steps:
@ -34,33 +34,67 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Grant systemd-journal access
run: sudo usermod -a -G systemd-journal "$USER" || echo 'no systemd-journal access'
- name: Python version - name: Python version
run: | run: |
F2B_PY=$(python -c "import sys; print(sys.version)") F2B_PY=$(python -c "import sys; print(sys.version)")
echo "Python: ${{ matrix.python-version }} -- $F2B_PY" echo "Python: ${{ matrix.python-version }} -- ${F2B_PY/$'\n'/ }"
F2B_PYV=$(echo "${F2B_PY}" | grep -oP '^\d+(?:\.\d+)')
F2B_PY=${F2B_PY:0:1} F2B_PY=${F2B_PY:0:1}
echo "Set F2B_PY=$F2B_PY" echo "Set F2B_PY=$F2B_PY, F2B_PYV=$F2B_PYV"
echo "F2B_PY=$F2B_PY" >> $GITHUB_ENV echo "F2B_PY=$F2B_PY" >> $GITHUB_ENV
echo "F2B_PYV=$F2B_PYV" >> $GITHUB_ENV
# for GHA we need to monitor all journals, since it cannot be found using SYSTEM_ONLY(4):
echo "F2B_SYSTEMD_DEFAULT_FLAGS=0" >> $GITHUB_ENV
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip if [[ "$F2B_PY" = 3 ]]; then python -m pip install --upgrade pip || echo "can't upgrade pip"; fi
if [[ "$F2B_PY" = 3 ]] && ! command -v 2to3x -v 2to3 > /dev/null; then if [[ "$F2B_PY" = 3 ]] && ! command -v 2to3x -v 2to3 > /dev/null; then
pip install 2to3 #pip install 2to3
sudo apt-get -y install 2to3
fi fi
pip install systemd-python || echo 'systemd not available' #sudo apt-get -y install python${F2B_PY/2/}-pyinotify || echo 'inotify not available'
pip install pyinotify || echo 'inotify not available' python -m pip install pyinotify || echo 'inotify not available'
#sudo apt-get -y install python${F2B_PY/2/}-systemd || echo 'systemd not available'
sudo apt-get -y install libsystemd-dev || echo 'systemd dependencies seems to be unavailable'
python -m pip install systemd-python || echo 'systemd not available'
#readline if available as module:
python -c 'import readline' 2> /dev/null || python -m pip install readline || echo 'readline not available'
- name: Before scripts - name: Before scripts
run: | run: |
cd "$GITHUB_WORKSPACE" cd "$GITHUB_WORKSPACE"
# Manually execute 2to3 for now # Manually execute 2to3 for now
if [[ "$F2B_PY" = 3 ]]; then echo "2to3 ..." && ./fail2ban-2to3; fi if [[ "$F2B_PY" = 3 ]]; then echo "2to3 ..." && ./fail2ban-2to3; fi
_debug() { echo -n "$1 "; err=$("${@:2}" 2>&1) && echo 'OK' || echo -e "FAIL\n$err"; }
# (debug) output current preferred encoding: # (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))' _debug 'Encodings:' python -c 'import locale, sys; from fail2ban.helpers import PREFER_ENC; print(PREFER_ENC, locale.getpreferredencoding(), (sys.stdout and sys.stdout.encoding))'
# (debug) backend availabilities:
echo 'Backends:'
_debug '- systemd:' python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
#_debug '- systemd (root): ' sudo python -c 'from fail2ban.server.filtersystemd import FilterSystemd'
_debug '- pyinotify:' python -c 'from fail2ban.server.filterpyinotify import FilterPyinotify'
- name: Test suite - name: Test suite
run: if [[ "$F2B_PY" = 2 ]]; then python setup.py test; else python bin/fail2ban-testcases --verbosity=2; fi run: |
if [[ "$F2B_PY" = 2 ]]; then
python setup.py test
elif dpkg --compare-versions "$F2B_PYV" lt 3.10; then
python bin/fail2ban-testcases --verbosity=2
else
echo "Skip systemd backend since systemd-python module must be fixed for python >= v.3.10 in GHA ..."
python bin/fail2ban-testcases --verbosity=2 -i "[sS]ystemd|[jJ]ournal"
fi
#- name: Test suite (debug some systemd tests only)
#run: python bin/fail2ban-testcases --verbosity=2 "[sS]ystemd|[jJ]ournal"
#run: python bin/fail2ban-testcases --verbosity=2 -l 5 "test_WrongChar"
- name: Build
run: python setup.py build
#- name: Test initd scripts #- name: Test initd scripts
# run: shellcheck -s bash -e SC1090,SC1091 files/debian-initd # run: shellcheck -s bash -e SC1090,SC1091 files/debian-initd

View File

@ -10,16 +10,8 @@ dist: xenial
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- python: 2.6
dist: trusty # required for Python 2.6
- python: 2.7 - python: 2.7
dist: trusty # required for packages like gamin #- python: pypy
name: 2.7 (trusty)
- python: 2.7
name: 2.7 (xenial)
- python: pypy
- python: 3.3
dist: trusty
- python: 3.4 - python: 3.4
- python: 3.5 - python: 3.5
- python: 3.6 - python: 3.6
@ -41,7 +33,8 @@ install:
# coverage # coverage
- travis_retry pip install coverage - travis_retry pip install coverage
# coveralls (note coveralls doesn't support 2.6 now): # coveralls (note coveralls doesn't support 2.6 now):
- if [[ $TRAVIS_PYTHON_VERSION != 2.6* ]]; then F2B_COV=1; else F2B_COV=0; fi #- if [[ $TRAVIS_PYTHON_VERSION != 2.6* ]]; then F2B_COV=1; else F2B_COV=0; fi
- F2B_COV=1
- if [[ "$F2B_COV" = 1 ]]; then travis_retry pip install coveralls; fi - if [[ "$F2B_COV" = 1 ]]; then travis_retry pip install coveralls; fi
# codecov: # codecov:
- travis_retry pip install codecov - travis_retry pip install codecov

166
ChangeLog
View File

@ -1,3 +1,4 @@
<!-- vim: syntax=Markdown -->
__ _ _ ___ _ __ _ _ ___ _
/ _|__ _(_) |_ ) |__ __ _ _ _ / _|__ _(_) |_ ) |__ __ _ _ _
| _/ _` | | |/ /| '_ \/ _` | ' \ | _/ _` | | |/ /| '_ \/ _` | ' \
@ -6,10 +7,171 @@
Fail2Ban: Changelog Fail2Ban: Changelog
=================== ===================
ver. 1.0.1 (2022/09/27) - energy-equals-mass-times-the-speed-of-light-squared
-----------
### Compatibility
* the minimum supported python version is now 2.7, if you have previous python version
you can use the 0.11 version of fail2ban or upgrade python (or even build it from source).
* potential incompatibility by parsing of options of `backend`, `filter` and `action` parameters (if they
are partially incorrect), because fail2ban could throw an error now (doesn't silently bypass it anymore).
* due to fix for CVE-2021-32749 (GHSA-m985-3f3v-cwmm) the mailing action using mailutils may require extra configuration,
if it is not compatible or doesn't support `-E 'set escape'` (e. g. with `mailcmd` parameter), see gh-3059
* automatic invocation of 2to3 is removed in setup now (gh-3098), there is also no option `--disable-2to3` anymore,
`./fail2ban-2to3` should be called outside before setup
* to v.0.11:
- due to change of `actioncheck` behavior (gh-488), some actions can be incompatible as regards
the invariant check, if `actionban` or `actionunban` would not throw an error (exit code
different from 0) in case of unsane environment.
- actions that have used tag `<ip>` (instead of `<fid>` or `<F-ID>`) to get failure-ID may become
incompatible, if filter uses IP-related tags (like `<ADDR>` or `<HOST>`) additionally to `<F-ID>`
and the values are different (gh-3217)
### Fixes
* theoretical RCE vulnerability in mailing action using mailutils (mail-whois), CVE-2021-32749, GHSA-m985-3f3v-cwmm
* readline fixed to consider interim new-line character as part of code point in multi-byte logs
(e. g. unicode encoding like utf-16be, utf-16le);
* [stability] solves race condition with uncontrolled growth of failure list (jail with too many matches,
that did not cause ban), behavior changed to ban ASAP, gh-2945
* fixes search for the best datepattern - e. g. if line is too short, boundaries check for previously known
unprecise pattern may fail on incomplete lines (logging break-off, no flush, etc), gh-3020
* [stability, performance] backend `systemd`:
- fixes error "local variable 'line' referenced before assignment", introduced in 55d7d9e2, gh-3097
- don't update database too often (every 10 ticks or ~ 10 seconds in production)
- fixes wrong time point of "in operation" mode, gh-2882
- better avoidance of landing in dead space by seeks over journals (improved seek to time)
- fixes missing space in message (tag `<matches>`) between timestamp and host if the message read from systemd journal, gh-3293
* [stability] backend `pyinotify`: fixes sporadic runtime error "dictionary changed size during iteration"
* several backends optimizations (in file and journal filters):
- don't need to wait if we still had log-entries from last iteration (which got interrupted for servicing)
- rewritten update log/journal position, it is more stable and faster now (fewer DB access and surely up-to-date at end)
* `paths-debian.conf`:
- add debian path to roundcube error logs
* `action.d/firewallcmd-*.conf` (multiport only): fixed port range selector, replacing `:` with `-`;"
reverted the incompatibility gh-3047 introduced in a038fd5, gh-2821, because this depends now on firewalld backend
(e. g. `-` vs. `:` related to `iptables` vs. `nftables`)
* `action.d/nginx-block-map.conf`: reload nginx only if it is running (also avoid error in nginx-errorlog, gh-2949)
* `action.d/ufw.conf`:
- fixed handling on IPv6 (using prepend, gh-2331, gh-3018)
- application names containing spaces can be used now (gh-656, gh-1532, gh-3018)
* `filter.d/apache-fakegooglebot.conf`:
- better, more precise regex and datepattern (closes possible weakness like gh-3013)
- `filter.d/ignorecommands/apache-fakegooglebot` - added timeout parameter (default 55 seconds), avoid fail with timeout
(default 1 minute) by reverse lookup on some slow DNS services (googlebots must be resolved fast), gh-2951
* `filter.d/apache-overflows.conf` - extended to match AH00126 error (Invalid URI ...), gh-2908
* `filter.d/asterisk.conf` - add transport to asterisk RE: call rejection messages can have the transport prefixed to the IP address, gh-2913
* `filter.d/courier-auth.conf`:
- consider optional port after IP, gh-3211
- regex is rewritten without catch-all's and right anchor, so it is more stable against further modifications now
* `filter.d/dovecot.conf`:
- adjusted for updated dovecot log format with `read(size=...)` in message (gh-3210)
- parse everything in parenthesis by auth-worker info, e. g. can match (pid=...,uid=...) too (amend to gh-2553)
- extended to match prefix like `conn unix:auth-worker (uid=143): auth-worker<13247>:`
(authenticate from external service like exim), gh-2553
- fixed "Authentication failure" regex, matches "Password mismatch" in title case (gh-2880)
* `filter.d/drupal-auth.conf` - more strict regex, extended to match "Login attempt failed from" (gh-2742)
* `filter.d/exim-common.conf` - pid-prefix extended to match `mx1 exim[...]:` (gh-2553)
* `filter.d/lighttpd-auth.conf` - adjusted to the current source code + avoiding catch-all's, etc (gh-3116)
* `filter.d/named-refused.conf`:
- added support for alternate names (suffix), FreeIPA renames the BIND9 named daemon to named-pkcs11, gh-2636
- fixes prefix for messages from systemd journal (no mandatory space ahead, because don't have timestamp), gh-2899
* `filter.d/nginx-*.conf` - added journalmatch to nginx filters, gh-2935
* `filter.d/nsd.conf` - support for current log format, gh-2965
* `filter.d/postfix.conf`: fixes and new vectors, review and combining several regex to single RE:
- mode `ddos` (and `aggressive`) extended:
* to consider abusive handling of clients hitting command limit, gh-3040
* to handle postscreen's PREGREET and HANGUP messages, gh-2898
- matches rejects with "undeliverable address" (sender/recipient verification) additionally to "Unknown user", gh-3039
both are configurable now via extended parameter and can be disabled using `exre-user=` supplied in filter parameters
- reject: BDAT/DATA from, gh-2927
- (since regex is more precise now) token selector changed to `[A-Z]{4}`, e. g. no matter what a command is supplied now
(RCPT, EHLO, VRFY, DATA, BDAT or something else)
- matches "Command rejected" and "Data command rejected" now
- matches RCPT from unknown, 504 5.5.2, need fully-qualified hostname, gh-2995
- matches 550 5.7.25 Client host rejected, gh-2996
* `filter.d/sendmail-auth.conf`:
- detect several "authentication failure" messages, sendmail 8.16.1, gh-2757
- detect user not found, gh-3030
- detect failures without user part, gh-3324
* `filter.d/sendmail-reject.conf`:
- fix reverse DNS for ... (gh-3012)
- fixed regex to consider "Connection rate limit exceeded" with different combination of arguments
* `filter.d/sshd.conf`:
- mode `ddos` extended - recognizes messages "kex_exchange_identification: Connection closed / reset by pear", gh-3086
(fixed possible regression of f77398c)
- mode `ddos` extended - recognizes new message "banner exchange: invalid format" generated by port scanner
(https payload on ssh port), gh-3169
* `filter.d/zoneminder.conf` - support new log format (ERR instead of WAR), add detection of non-existent user login attempts, gh-2984
* amend to gh-980 fixing several actions (correctly supporting new enhancements now)
* fixed typo by `--dump-pretty` option which did never work (only `--dp` was working)
* fixes start of fail2ban-client in docker: speedup daemonization process by huge open files limit, gh-3334
* provides details of failed regex compilation in the error message we throw in Regex-constructor
(it's good to know what exactly is wrong)
* fixed failed update of database didn't signal with an error, gh-3352:
- client and server exit with error code by failure during start process (in foreground mode)
- added fallback to repair if database cannot be upgraded
### New Features and Enhancements
* python 3.10 and 3.11 compatibility (and GHA-CI support)
* `actioncheck` behavior is changed now (gh-488), so invariant check as well as restore or repair
of sane environment (in case of recognized unsane state) would only occur on action errors (e. g.
if ban or unban operations are exiting with other code as 0)
* better recognition of log rotation, better performance by reopen: avoid unnecessary seek to begin of file
(and hash calculation)
* file filter reads only complete lines (ended with new-line) now, so waits for end of line (for its completion)
* datedetector:
- token `%Z` must recognize zone abbreviation `Z` (GMT/UTC) also (similar to `%z`)
- token `%Z` recognizes all known zone abbreviation besides Z, GMT, UTC correctly, if it is matching
(`%z` remains unchanged for backwards-compatibility, see comment in code)
- date patterns `%ExY` and `%Exy` accept every year from 19xx up to current century (+3 years) in `fail2ban-regex`
- better grouping algorithm for resulting century RE for `%ExY` and `%Exy`
* actions differentiate tags `<ip>` and `<fid>` (`<F-ID>`), if IP-address deviates from ID then the value
of `<ip>` is not equal `<fid>` anymore (gh-3217)
* action info extended with new members for jail info (usable as tags in command actions), gh-10:
- `<jail.found>`, `<jail.found_total>` - current and total found failures
- `<jail.banned>`, `<jail.banned_total>` - current and total bans
* `filter.d/monitorix.conf` - added new filter and jail for Monitorix, gh-2679
* `filter.d/mssql-auth.conf` - new filter and jail for Microsoft SQL Server, gh-2642
* `filter.d/nginx-bad-request.conf` - added filter to find bad requests (400), gh-2750
* `filter.d/nginx-http-auth.conf` - extended with parameter mode, so additionally to `auth` (or `normal`)
mode `fallback` (or combined as `aggressive`) can find SSL errors while SSL handshaking, gh-2881
* `filter.d/scanlogd.conf` - new filter and jail, add support for filtering out detected port scans via scanlogd, gh-2950
* `action.d/apprise.conf` - added Apprise support (50+ Notifications), gh-2565
* `action.d/badips.*` - removed actions, badips.com is no longer active, gh-2889
* `action.d/cloudflare.conf` - better IPv6 capability, gh-2891
* `action.d/cloudflare-token.conf` - added support for Cloudflare Token APIs. This method is more restrictive and therefore safter than using API Keys.
* `action.d/ipthreat.conf` - new action for IPThreat integration, gh-3349
* `action.d/ufw.conf` (gh-3018):
- new option `add` (default `prepend`), can be supplied as `insert 1` for ufw versions before v.0.36 (gh-2331, gh-3018)
- new options `kill-mode` and `kill` to drop established connections of intruder (see action for details, gh-3018)
* `iptables` and `iptables-ipset` actions extended to support multiple protocols with single action
for multiport or oneport type (back-ported from nftables action);
* `iptables` actions are more breakdown-safe: start wouldn't fail if chain or rule already exists
(e. g. created by previous instance and doesn't get purged properly); ultimately closes gh-980
* `ipset` actions are more breakdown-safe: start wouldn't fail if set with this name already exists
(e. g. created by previous instance and don't deleted properly)
* replace internals of several `iptables` and `iptables-ipset` actions using internals of iptables include:
- better check mechanism (using `-C`, option `--check` is available long time);
- additionally iptables-ipset is a common action for `iptables-ipset-proto6-*` now (which become obsolete now);
- many features of different iptables actions are combinable as single chain/rule (can be supplied to action as parameters);
- iptables is a replacement for iptables-common now, several actions using this as include now become obsolete;
* new logtarget SYSTEMD-JOURNAL, gh-1403
* fail2ban.conf: new fail2ban configuration option `allowipv6` (default `auto`), can be used to allow or disallow IPv6
interface in fail2ban immediately by start (e. g. if fail2ban starts before network interfaces), gh-2804
* invalidate IP/DNS caches by reload, so inter alia would allow to recognize IPv6IsAllowed immediately, previously
retarded up to cache max-time (5m), gh-2804
* OpenRC (Gentoo, mainly) service script improvements, gh-2182
* suppress unneeded info "Jail is not a JournalFilter instance" (moved to debug level), gh-3186
* implements new interpolation variable `%(fail2ban_confpath)s` (automatically substituted from config-reader path,
default `/etc/fail2ban` or `/usr/local/etc/fail2ban` depending on distribution); `ignorecommands_dir` is unneeded anymore,
thus removed from `paths-common.conf`, fixes gh-3005
* `fail2ban-regex`: accepts filter parameters containing new-line
ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools ver. 0.11.2 (2020/11/23) - heal-the-world-with-security-tools
----------- -----------
### Compatibility: ### Compatibility
* to v.0.10: * to v.0.10:
- 0.11 is totally compatible to 0.10 (configuration- and API-related stuff), but the database - 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 got some new tables and fields (auto-converted during the first start), so once updated to 0.11, you
@ -189,7 +351,7 @@ Yes, Hrrrm...
### New Features ### New Features
* new replacement tags for failregex to match subnets in form of IP-addresses with CIDR mask (gh-2559): * 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); - `<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); - `<SUBNET>` - regex to match sub-net addresses (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 * 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 * 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) (ATM used similar to tag `<F-NOFAIL>`, but it does not add the log-line to matches, gh-2279)

View File

@ -278,6 +278,7 @@ to tune it. fail2ban-regex -D ... will present Debuggex URLs for the regexs
and sample log files that you pass into it. and sample log files that you pass into it.
In general use when using regex debuggers for generating fail2ban filters: In general use when using regex debuggers for generating fail2ban filters:
* use regex from the ./fail2ban-regex output (to ensure all substitutions are * use regex from the ./fail2ban-regex output (to ensure all substitutions are
done) done)
* replace <HOST> with (?&.ipv4) * replace <HOST> with (?&.ipv4)

View File

@ -5,11 +5,11 @@ bin/fail2ban-testcases
ChangeLog ChangeLog
config/action.d/abuseipdb.conf config/action.d/abuseipdb.conf
config/action.d/apf.conf config/action.d/apf.conf
config/action.d/badips.conf config/action.d/apprise.conf
config/action.d/badips.py
config/action.d/blocklist_de.conf config/action.d/blocklist_de.conf
config/action.d/bsd-ipfw.conf config/action.d/bsd-ipfw.conf
config/action.d/cloudflare.conf config/action.d/cloudflare.conf
config/action.d/cloudflare-token.conf
config/action.d/complain.conf config/action.d/complain.conf
config/action.d/dshield.conf config/action.d/dshield.conf
config/action.d/dummy.conf config/action.d/dummy.conf
@ -25,8 +25,8 @@ config/action.d/hostsdeny.conf
config/action.d/ipfilter.conf config/action.d/ipfilter.conf
config/action.d/ipfw.conf config/action.d/ipfw.conf
config/action.d/iptables-allports.conf config/action.d/iptables-allports.conf
config/action.d/iptables-common.conf
config/action.d/iptables.conf config/action.d/iptables.conf
config/action.d/iptables-ipset.conf
config/action.d/iptables-ipset-proto4.conf config/action.d/iptables-ipset-proto4.conf
config/action.d/iptables-ipset-proto6-allports.conf config/action.d/iptables-ipset-proto6-allports.conf
config/action.d/iptables-ipset-proto6.conf config/action.d/iptables-ipset-proto6.conf
@ -34,6 +34,7 @@ config/action.d/iptables-multiport.conf
config/action.d/iptables-multiport-log.conf config/action.d/iptables-multiport-log.conf
config/action.d/iptables-new.conf config/action.d/iptables-new.conf
config/action.d/iptables-xt_recent-echo.conf config/action.d/iptables-xt_recent-echo.conf
config/action.d/ipthreat.conf
config/action.d/mail-buffered.conf config/action.d/mail-buffered.conf
config/action.d/mail.conf config/action.d/mail.conf
config/action.d/mail-whois-common.conf config/action.d/mail-whois-common.conf
@ -112,10 +113,13 @@ config/filter.d/kerio.conf
config/filter.d/lighttpd-auth.conf config/filter.d/lighttpd-auth.conf
config/filter.d/mongodb-auth.conf config/filter.d/mongodb-auth.conf
config/filter.d/monit.conf config/filter.d/monit.conf
config/filter.d/monitorix.conf
config/filter.d/mssql-auth.conf
config/filter.d/murmur.conf config/filter.d/murmur.conf
config/filter.d/mysqld-auth.conf config/filter.d/mysqld-auth.conf
config/filter.d/nagios.conf config/filter.d/nagios.conf
config/filter.d/named-refused.conf config/filter.d/named-refused.conf
config/filter.d/nginx-bad-request.conf
config/filter.d/nginx-botsearch.conf config/filter.d/nginx-botsearch.conf
config/filter.d/nginx-http-auth.conf config/filter.d/nginx-http-auth.conf
config/filter.d/nginx-limit-req.conf config/filter.d/nginx-limit-req.conf
@ -134,6 +138,7 @@ config/filter.d/pure-ftpd.conf
config/filter.d/qmail.conf config/filter.d/qmail.conf
config/filter.d/recidive.conf config/filter.d/recidive.conf
config/filter.d/roundcube-auth.conf config/filter.d/roundcube-auth.conf
config/filter.d/scanlogd.conf
config/filter.d/screensharingd.conf config/filter.d/screensharingd.conf
config/filter.d/selinux-common.conf config/filter.d/selinux-common.conf
config/filter.d/selinux-ssh.conf config/filter.d/selinux-ssh.conf
@ -220,7 +225,6 @@ fail2ban/setup.py
fail2ban-testcases-all fail2ban-testcases-all
fail2ban-testcases-all-python3 fail2ban-testcases-all-python3
fail2ban/tests/action_d/__init__.py fail2ban/tests/action_d/__init__.py
fail2ban/tests/action_d/test_badips.py
fail2ban/tests/action_d/test_smtp.py fail2ban/tests/action_d/test_smtp.py
fail2ban/tests/actionstestcase.py fail2ban/tests/actionstestcase.py
fail2ban/tests/actiontestcase.py fail2ban/tests/actiontestcase.py
@ -317,10 +321,13 @@ fail2ban/tests/files/logs/kerio
fail2ban/tests/files/logs/lighttpd-auth fail2ban/tests/files/logs/lighttpd-auth
fail2ban/tests/files/logs/mongodb-auth fail2ban/tests/files/logs/mongodb-auth
fail2ban/tests/files/logs/monit fail2ban/tests/files/logs/monit
fail2ban/tests/files/logs/monitorix
fail2ban/tests/files/logs/mssql-auth
fail2ban/tests/files/logs/murmur fail2ban/tests/files/logs/murmur
fail2ban/tests/files/logs/mysqld-auth fail2ban/tests/files/logs/mysqld-auth
fail2ban/tests/files/logs/nagios fail2ban/tests/files/logs/nagios
fail2ban/tests/files/logs/named-refused fail2ban/tests/files/logs/named-refused
fail2ban/tests/files/logs/nginx-bad-request
fail2ban/tests/files/logs/nginx-botsearch fail2ban/tests/files/logs/nginx-botsearch
fail2ban/tests/files/logs/nginx-http-auth fail2ban/tests/files/logs/nginx-http-auth
fail2ban/tests/files/logs/nginx-limit-req fail2ban/tests/files/logs/nginx-limit-req
@ -339,6 +346,7 @@ fail2ban/tests/files/logs/pure-ftpd
fail2ban/tests/files/logs/qmail fail2ban/tests/files/logs/qmail
fail2ban/tests/files/logs/recidive fail2ban/tests/files/logs/recidive
fail2ban/tests/files/logs/roundcube-auth fail2ban/tests/files/logs/roundcube-auth
fail2ban/tests/files/logs/scanlogd
fail2ban/tests/files/logs/screensharingd fail2ban/tests/files/logs/screensharingd
fail2ban/tests/files/logs/selinux-ssh fail2ban/tests/files/logs/selinux-ssh
fail2ban/tests/files/logs/sendmail-auth fail2ban/tests/files/logs/sendmail-auth
@ -391,12 +399,12 @@ files/cacti/fail2ban_stats.sh
files/cacti/README files/cacti/README
files/debian-initd files/debian-initd
files/fail2ban-logrotate files/fail2ban-logrotate
files/fail2ban-openrc.conf
files/fail2ban-openrc.init.in
files/fail2ban.service.in files/fail2ban.service.in
files/fail2ban-tmpfiles.conf files/fail2ban-tmpfiles.conf
files/fail2ban.upstart files/fail2ban.upstart
files/gen_badbots files/gen_badbots
files/gentoo-confd
files/gentoo-initd
files/ipmasq-ZZZzzz_fail2ban.rul files/ipmasq-ZZZzzz_fail2ban.rul
files/logwatch/fail2ban files/logwatch/fail2ban
files/logwatch/fail2ban-0.8.log files/logwatch/fail2ban-0.8.log

View File

@ -2,7 +2,7 @@
/ _|__ _(_) |_ ) |__ __ _ _ _ / _|__ _(_) |_ ) |__ __ _ _ _
| _/ _` | | |/ /| '_ \/ _` | ' \ | _/ _` | | |/ /| '_ \/ _` | ' \
|_| \__,_|_|_/___|_.__/\__,_|_||_| |_| \__,_|_|_/___|_.__/\__,_|_||_|
v0.11.0.dev1 20??/??/?? v1.0.1.dev1 20??/??/??
## Fail2Ban: ban hosts that cause multiple authentication errors ## Fail2Ban: ban hosts that cause multiple authentication errors
@ -33,7 +33,8 @@ Installation:
this case, you should use that instead.** this case, you should use that instead.**
Required: Required:
- [Python2 >= 2.6 or Python >= 3.2](https://www.python.org) or [PyPy](https://pypy.org) - [Python2 >= 2.7 or Python >= 3.2](https://www.python.org) or [PyPy](https://pypy.org)
- python-setuptools, python-distutils or python3-setuptools for installation from source
Optional: Optional:
- [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require: - [pyinotify >= 0.8.3](https://github.com/seb-m/pyinotify), may require:
@ -46,11 +47,11 @@ Optional:
To install: To install:
tar xvfj fail2ban-0.11.0.tar.bz2 tar xvfj fail2ban-1.0.1.tar.bz2
cd fail2ban-0.11.0 cd fail2ban-1.0.1
sudo python setup.py install 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 Alternatively, you can clone the source from GitHub to a directory of Your choice, and do the install from there. Pick the correct branch, for example, master or 0.11
git clone https://github.com/fail2ban/fail2ban.git git clone https://github.com/fail2ban/fail2ban.git
cd fail2ban cd fail2ban
@ -89,11 +90,11 @@ fail2ban(1) and jail.conf(5) manpages for further references.
Code status: Code status:
------------ ------------
* travis-ci.org: [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.11)](https://travis-ci.org/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.10)](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) (0.10 branch) * travis-ci.org: [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=master)](https://travis-ci.org/fail2ban/fail2ban?branch=master) / [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.11)](https://travis-ci.org/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![tests status](https://secure.travis-ci.org/fail2ban/fail2ban.svg?branch=0.10)](https://travis-ci.org/fail2ban/fail2ban?branch=0.10) (0.10 branch)
* coveralls.io: [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.11)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.10)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10) / (0.10 branch) * coveralls.io: [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=master)](https://coveralls.io/github/fail2ban/fail2ban?branch=master) / [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.11)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.11) (0.11 branch) / [![Coverage Status](https://coveralls.io/repos/fail2ban/fail2ban/badge.svg?branch=0.10)](https://coveralls.io/github/fail2ban/fail2ban?branch=0.10) / (0.10 branch)
* codecov.io: [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.11)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.11) (0.11 branch) / [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.10)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10) (0.10 branch) * codecov.io: [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=master)](https://codecov.io/gh/fail2ban/fail2ban/branch/master) / [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.11)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.11) (0.11 branch) / [![codecov.io](https://codecov.io/gh/fail2ban/fail2ban/coverage.svg?branch=0.10)](https://codecov.io/gh/fail2ban/fail2ban/branch/0.10) (0.10 branch)
Contact: Contact:
-------- --------

1
THANKS
View File

@ -33,6 +33,7 @@ Christoph Haas
Christos Psonis Christos Psonis
craneworks craneworks
Cyril Jaquier Cyril Jaquier
Daniel Aleksandersen
Daniel B. Cid Daniel B. Cid
Daniel B. Daniel B.
Daniel Black Daniel Black

View File

@ -0,0 +1,49 @@
# Fail2Ban configuration file
#
# Author: Chris Caron <lead2gold@gmail.com>
#
#
[Definition]
# Option: actionstart
# Notes.: command executed once at the start of Fail2Ban.
# Values: CMD
#
actionstart = printf %%b "The jail <name> as been started successfully." | <apprise> -t "[Fail2Ban] <name>: started on `uname -n`"
# Option: actionstop
# Notes.: command executed once at the end of Fail2Ban
# Values: CMD
#
actionstop = printf %%b "The jail <name> has been stopped." | <apprise> -t "[Fail2Ban] <name>: stopped on `uname -n`"
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# 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 = printf %%b "The IP <ip> has just been banned by Fail2Ban after <failures> attempts against <name>" | <apprise> -n "warning" -t "[Fail2Ban] <name>: banned <ip> from `uname -n`"
# 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 =
[Init]
# Define location of the default apprise configuration file to use
#
config = /etc/fail2ban/apprise.conf
#
apprise = apprise -c "<config>"

View File

@ -1,19 +0,0 @@
# Fail2ban reporting to badips.com
#
# Note: This reports an IP only and does not actually ban traffic. Use
# another action in the same jail if you want bans to occur.
#
# Set the category to the appropriate value before use.
#
# To get see register and optional key to get personalised graphs see:
# http://www.badips.com/blog/personalized-statistics-track-the-attackers-of-all-your-servers-with-one-key
[Definition]
actionban = curl --fail --user-agent "<agent>" http://www.badips.com/add/<category>/<ip>
[Init]
# Option: category
# Notes.: Values are from the list here: http://www.badips.com/get/categories
category =

View File

@ -1,391 +0,0 @@
# 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.
import sys
if sys.version_info < (2, 7): # pragma: no cover
raise ImportError("badips.py action requires Python >= 2.7")
import json
import threading
import logging
if sys.version_info >= (3, ): # pragma: 2.x no cover
from urllib.request import Request, urlopen
from urllib.parse import urlencode
from urllib.error import HTTPError
else: # pragma: 3.x no cover
from urllib2 import Request, urlopen, HTTPError
from urllib import urlencode
from fail2ban.server.actions import Actions, ActionBase, BanTicket
from fail2ban.helpers import splitwords, str2LogLevel
class BadIPsAction(ActionBase): # pragma: no cover - may be unavailable
"""Fail2Ban action which reports bans to badips.com, and also
blacklist bad IPs listed on badips.com by using another action's
ban method.
Parameters
----------
jail : Jail
The jail which the action belongs to.
name : str
Name assigned to the action.
category : str
Valid badips.com category for reporting failures.
score : int, optional
Minimum score for bad IPs. Default 3.
age : str, optional
Age of last report for bad IPs, per badips.com syntax.
Default "24h" (24 hours)
banaction : str, optional
Name of banaction to use for blacklisting bad IPs. If `None`,
no blacklist of IPs will take place.
Default `None`.
bancategory : str, optional
Name of category to use for blacklisting, which can differ
from category used for reporting. e.g. may want to report
"postfix", but want to use whole "mail" category for blacklist.
Default `category`.
bankey : str, optional
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.`
Raises
------
ValueError
If invalid `category`, `score`, `banaction` or `updateperiod`.
"""
TIMEOUT = 10
_badips = "https://www.badips.com"
def _Request(self, url, **argv):
return Request(url, headers={'User-Agent': self.agent}, **argv)
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
self.agent = agent
self.category = category
self.score = score
self.age = age
self.banaction = banaction
self.bancategory = bancategory or category
self.bankey = bankey
loglevel = splitwords(loglevel)
self.sumloglevel = str2LogLevel(loglevel[-1])
self.loglevel = str2LogLevel(loglevel[0])
self.updateperiod = updateperiod
self._bannedips = set()
# Used later for threading.Timer for updating badips
self._timer = None
@staticmethod
def isAvailable(timeout=1):
try:
response = urlopen(Request("/".join([BadIPsAction._badips]),
headers={'User-Agent': "Fail2Ban"}), timeout=timeout)
return True, ''
except Exception as e: # pragma: no cover
return False, e
def logError(self, response, what=''): # pragma: no cover - sporadical (502: Bad Gateway, etc)
messages = {}
try:
messages = json.loads(response.read().decode('utf-8'))
except:
pass
self._logSys.error(
"%s. badips.com response: '%s'", what,
messages.get('err', 'Unknown'))
def getCategories(self, incParents=False):
"""Get badips.com categories.
Returns
-------
set
Set of categories.
Raises
------
HTTPError
Any issues with badips.com request.
ValueError
If badips.com response didn't contain necessary information
"""
try:
response = urlopen(
self._Request("/".join([self._badips, "get", "categories"])), timeout=self.timeout)
except HTTPError as response: # pragma: no cover
self.logError(response, "Failed to fetch categories")
raise
else:
response_json = json.loads(response.read().decode('utf-8'))
if not 'categories' in response_json:
err = "badips.com response lacked categories specification. Response was: %s" \
% (response_json,)
self._logSys.error(err)
raise ValueError(err)
categories = response_json['categories']
categories_names = set(
value['Name'] for value in categories)
if incParents:
categories_names.update(set(
value['Parent'] for value in categories
if "Parent" in value))
return categories_names
def getList(self, category, score, age, key=None):
"""Get badips.com list of bad IPs.
Parameters
----------
category : str
Valid badips.com category.
score : int
Minimum score for bad IPs.
age : str
Age of last report for bad IPs, per badips.com syntax.
key : str, optional
Key issued by badips.com to fetch IPs reported with the
associated key.
Returns
-------
set
Set of bad IPs.
Raises
------
HTTPError
Any issues with badips.com request.
"""
try:
url = "?".join([
"/".join([self._badips, "get", "list", category, str(score)]),
urlencode({'age': age})])
if key:
url = "&".join([url, urlencode({'key': key})])
self._logSys.debug('badips.com: get list, url: %r', url)
response = urlopen(self._Request(url), timeout=self.timeout)
except HTTPError as response: # pragma: no cover
self.logError(response, "Failed to fetch bad IP list")
raise
else:
return set(response.read().decode('utf-8').split())
@property
def category(self):
"""badips.com category for reporting IPs.
"""
return self._category
@category.setter
def category(self, category):
if category not in self.getCategories():
self._logSys.error("Category name '%s' not valid. "
"see badips.com for list of valid categories",
category)
raise ValueError("Invalid category: %s" % category)
self._category = category
@property
def bancategory(self):
"""badips.com bancategory for fetching IPs.
"""
return self._bancategory
@bancategory.setter
def bancategory(self, bancategory):
if bancategory != "any" and bancategory not in self.getCategories(incParents=True):
self._logSys.error("Category name '%s' not valid. "
"see badips.com for list of valid categories",
bancategory)
raise ValueError("Invalid bancategory: %s" % bancategory)
self._bancategory = bancategory
@property
def score(self):
"""badips.com minimum score for fetching IPs.
"""
return self._score
@score.setter
def score(self, score):
score = int(score)
if 0 <= score <= 5:
self._score = score
else:
raise ValueError("Score must be 0-5")
@property
def banaction(self):
"""Jail action to use for banning/unbanning.
"""
return self._banaction
@banaction.setter
def banaction(self, banaction):
if banaction is not None and banaction not in self._jail.actions:
self._logSys.error("Action name '%s' not in jail '%s'",
banaction, self._jail.name)
raise ValueError("Invalid banaction")
self._banaction = banaction
@property
def updateperiod(self):
"""Period in seconds between banned bad IPs will be updated.
"""
return self._updateperiod
@updateperiod.setter
def updateperiod(self, updateperiod):
updateperiod = int(updateperiod)
if updateperiod > 0:
self._updateperiod = updateperiod
else:
raise ValueError("Update period must be integer greater than 0")
def _banIPs(self, ips):
for ip in ips:
try:
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",
ip, self._jail.name, self.banaction, e,
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
else:
self._bannedips.add(ip)
self._logSys.log(self.loglevel,
"Banned IP %s for jail '%s' with action '%s'",
ip, self._jail.name, self.banaction)
def _unbanIPs(self, ips):
for ip in ips:
try:
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",
ip, self._jail.name, self.banaction, e,
exc_info=self._logSys.getEffectiveLevel()<=logging.DEBUG)
else:
self._logSys.log(self.loglevel,
"Unbanned IP %s for jail '%s' with action '%s'",
ip, self._jail.name, self.banaction)
finally:
self._bannedips.remove(ip)
def start(self):
"""If `banaction` set, blacklists bad IPs.
"""
if self.banaction is not None:
self.update()
def update(self):
"""If `banaction` set, updates blacklisted IPs.
Queries badips.com for list of bad IPs, removing IPs from the
blacklist if no longer present, and adds new bad IPs to the
blacklist.
"""
if self.banaction is not None:
if self._timer:
self._timer.cancel()
self._timer = None
try:
ips = self.getList(
self.bancategory, self.score, self.age, self.bankey)
# Remove old IPs no longer listed
s = self._bannedips - ips
m = len(s)
self._unbanIPs(s)
# Add new IPs which are now listed
s = ips - self._bannedips
p = len(s)
self._banIPs(s)
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()
def stop(self):
"""If `banaction` set, clears blacklisted IPs.
"""
if self.banaction is not None:
if self._timer:
self._timer.cancel()
self._timer = None
self._unbanIPs(self._bannedips.copy())
def ban(self, aInfo):
"""Reports banned IP to badips.com.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
Raises
------
HTTPError
Any issues with badips.com request.
"""
try:
url = "/".join([self._badips, "add", self.category, str(aInfo['ip'])])
self._logSys.debug('badips.com: ban, url: %r', url)
response = urlopen(self._Request(url), timeout=self.timeout)
except HTTPError as response: # pragma: no cover
self.logError(response, "Failed to ban")
raise
else:
messages = json.loads(response.read().decode('utf-8'))
self._logSys.debug(
"Response from badips.com report: '%s'",
messages['suc'])
Action = BadIPsAction

View File

@ -0,0 +1,92 @@
#
# Author: Logic-32
#
# IMPORTANT
#
# Please set jail.local's permission to 640 because it contains your CF API token.
#
# This action depends on curl.
#
# To get your Cloudflare API token: https://developers.cloudflare.com/api/tokens/create/
#
# Cloudflare Firewall API: https://developers.cloudflare.com/firewall/api/cf-firewall-rules/endpoints/
[Definition]
# 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 =
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop =
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# Option: actionban
# Notes.: command executed when banning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
actionban = curl -s -X POST "<_cf_api_url>" \
<_cf_api_prms> \
--data '{"mode":"<cfmode>","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"<notes>"}'
# Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the
# command is executed with Fail2Ban user rights.
# Tags: <ip> IP address
# <failures> number of failures
# <time> unix timestamp of the ban time
# Values: CMD
#
actionunban = id=$(curl -s -X GET "<_cf_api_url>?mode=<cfmode>&notes=<notes>&configuration.target=<cftarget>&configuration.value=<ip>" \
<_cf_api_prms> \
| awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'id'\042/){print $(i+1)}}}' \
| tr -d ' "' \
| head -n 1)
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found using target <cftarget>"; exit 0; fi; \
curl -s -X DELETE "<_cf_api_url>/$id" \
<_cf_api_prms> \
--data '{"cascade": "none"}'
_cf_api_url = https://api.cloudflare.com/client/v4/zones/<cfzone>/firewall/access_rules/rules
_cf_api_prms = -H "Authorization: Bearer <cftoken>" -H "Content-Type: application/json"
[Init]
# Declare your Cloudflare Authorization Bearer Token in the [DEFAULT] section of your jail.local file.
# The Cloudflare <ZONE_ID> of hte domain you want to manage.
#
# cfzone =
# Your personal Cloudflare token. Ideally restricted to just have "Zone.Firewall Services" permissions.
#
# cftoken =
# Target of the firewall rule. Default is "ip" (v4).
#
cftarget = ip
# The firewall mode Cloudflare should use. Default is "block" (deny access).
# Consider also "js_challenge" or other "allowed_modes" if you want.
#
cfmode = block
# The message to include in the firewall IP banning rule.
#
notes = Fail2Ban <name>
[Init?family=inet6]
cftarget = ip6

View File

@ -44,7 +44,7 @@ actioncheck =
#actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>' #actionban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=ban' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4 # API v4
actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \ actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
-d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' \ -d '{"mode":"block","configuration":{"target":"<cftarget>","value":"<ip>"},"notes":"Fail2Ban <name>"}' \
<_cf_api_url> <_cf_api_url>
# Option: actionunban # Option: actionunban
@ -59,7 +59,7 @@ actionban = curl -s -o /dev/null -X POST <_cf_api_prms> \
#actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>' #actionunban = curl -s -o /dev/null https://www.cloudflare.com/api_json.html -d 'a=nul' -d 'tkn=<cftoken>' -d 'email=<cfuser>' -d 'key=<ip>'
# API v4 # API v4
actionunban = id=$(curl -s -X GET <_cf_api_prms> \ actionunban = id=$(curl -s -X GET <_cf_api_prms> \
"<_cf_api_url>?mode=block&configuration_target=ip&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \ "<_cf_api_url>?mode=block&configuration_target=<cftarget>&configuration_value=<ip>&page=1&per_page=1&notes=Fail2Ban%%20<name>" \
| { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; }) | { jq -r '.result[0].id' 2>/dev/null || tr -d '\n' | sed -nE 's/^.*"result"\s*:\s*\[\s*\{\s*"id"\s*:\s*"([^"]+)".*$/\1/p'; })
if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi; if [ -z "$id" ]; then echo "<name>: id for <ip> cannot be found"; exit 0; fi;
curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id" curl -s -o /dev/null -X DELETE <_cf_api_prms> "<_cf_api_url>/$id"
@ -81,3 +81,8 @@ _cf_api_prms = -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' -H 'Conten
cftoken = cftoken =
cfuser = cfuser =
cftarget = ip
[Init?family=inet6]
cftarget = ip6

View File

@ -102,7 +102,7 @@ logpath = /dev/null
# Notes.: Your system mail command. Is passed 2 args: subject and recipient # Notes.: Your system mail command. Is passed 2 args: subject and recipient
# Values: CMD # Values: CMD
# #
mailcmd = mail -s mailcmd = mail -E 'set escape' -s
# Option: mailargs # Option: mailargs
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail: # Notes.: Additional arguments to mail command. e.g. for standard Unix mail:

View File

@ -179,7 +179,7 @@ tcpflags =
# Notes.: Your system mail command. Is passed 2 args: subject and recipient # Notes.: Your system mail command. Is passed 2 args: subject and recipient
# Values: CMD # Values: CMD
# #
mailcmd = mail -s mailcmd = mail -E 'set escape' -s
# Option: mailargs # Option: mailargs
# Notes.: Additional arguments to mail command. e.g. for standard Unix mail: # Notes.: Additional arguments to mail command. e.g. for standard Unix mail:

View File

@ -18,20 +18,45 @@ before = firewallcmd-common.conf
[Definition] [Definition]
actionstart = ipset create <ipmset> hash:ip timeout <default-ipsettime> <familyopt> actionstart = <ipstype_<ipsettype>/actionstart>
firewall-cmd --direct --add-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype> firewall-cmd --direct --add-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
actionflush = ipset flush <ipmset> actionflush = <ipstype_<ipsettype>/actionflush>
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype> actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 <actiontype> -m set --match-set <ipmset> src -j <blocktype>
<actionflush> <actionflush>
ipset destroy <ipmset> <ipstype_<ipsettype>/actionstop>
actionban = ipset add <ipmset> <ip> timeout <ipsettime> -exist actionban = <ipstype_<ipsettype>/actionban>
# actionprolong = %(actionban)s # actionprolong = %(actionban)s
actionunban = ipset del <ipmset> <ip> -exist actionunban = <ipstype_<ipsettype>/actionunban>
[ipstype_ipset]
actionstart = ipset -exist create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
actionflush = ipset flush <ipmset>
actionstop = ipset destroy <ipmset>
actionban = ipset -exist add <ipmset> <ip> timeout <ipsettime>
actionunban = ipset -exist del <ipmset> <ip>
[ipstype_firewalld]
actionstart = firewall-cmd --direct --new-ipset=<ipmset> --type=hash:ip --option=timeout=<default-ipsettime> <firewalld_familyopt>
# TODO: there doesn't seem to be an explicit way to invoke the ipset flush function using firewall-cmd
actionflush =
actionstop = firewall-cmd --direct --delete-ipset=<ipmset>
actionban = firewall-cmd --ipset=<ipmset> --add-entry=<ip>
actionunban = firewall-cmd --ipset=<ipmset> --remove-entry=<ip>
[Init] [Init]
@ -56,6 +81,12 @@ ipsettime = 0
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>'] # banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0) timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
# Option: ipsettype
# Notes.: defines type of ipset used for match-set (firewalld or ipset)
# Values: firewalld or ipset
# Default: ipset
ipsettype = ipset
# Option: actiontype # Option: actiontype
# Notes.: defines additions to the blocking rule # Notes.: defines additions to the blocking rule
# Values: leave empty to block all attempts from the host # Values: leave empty to block all attempts from the host
@ -71,18 +102,20 @@ allports = -p <protocol>
# Option: multiport # Option: multiport
# Notes.: addition to block access only to specific ports # Notes.: addition to block access only to specific ports
# Usage.: use in jail config: banaction = firewallcmd-ipset[actiontype=<multiport>] # Usage.: use in jail config: banaction = firewallcmd-ipset[actiontype=<multiport>]
multiport = -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" multiport = -p <protocol> -m multiport --dports <port>
ipmset = f2b-<name> ipmset = f2b-<name>
familyopt = familyopt =
firewalld_familyopt =
[Init?family=inet6] [Init?family=inet6]
ipmset = f2b-<name>6 ipmset = f2b-<name>6
familyopt = family inet6 familyopt = family inet6
firewalld_familyopt = --option=family=inet6
# DEV NOTES: # DEV NOTES:
# #
# Author: Edgar Hoch and Daniel Black # Author: Edgar Hoch, Daniel Black, Sergey Brester and Mihail Politaev
# firewallcmd-new / iptables-ipset-proto6 combined for maximium goodness # firewallcmd-new / iptables-ipset-proto6 combined for maximium goodness

View File

@ -11,9 +11,9 @@ before = firewallcmd-common.conf
actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name> actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name>
firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name> firewall-cmd --direct --add-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name> actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m conntrack --ctstate NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
firewall-cmd --direct --remove-rules <family> filter f2b-<name> firewall-cmd --direct --remove-rules <family> filter f2b-<name>
firewall-cmd --direct --remove-chain <family> filter f2b-<name> firewall-cmd --direct --remove-chain <family> filter f2b-<name>

View File

@ -10,9 +10,9 @@ before = firewallcmd-common.conf
actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name> actionstart = firewall-cmd --direct --add-chain <family> filter f2b-<name>
firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN firewall-cmd --direct --add-rule <family> filter f2b-<name> 1000 -j RETURN
firewall-cmd --direct --add-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name> firewall-cmd --direct --add-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports "$(echo '<port>' | sed s/:/-/g)" -j f2b-<name> actionstop = firewall-cmd --direct --remove-rule <family> filter <chain> 0 -m state --state NEW -p <protocol> -m multiport --dports <port> -j f2b-<name>
firewall-cmd --direct --remove-rules <family> filter f2b-<name> firewall-cmd --direct --remove-rules <family> filter f2b-<name>
firewall-cmd --direct --remove-chain <family> filter f2b-<name> firewall-cmd --direct --remove-chain <family> filter f2b-<name>

View File

@ -37,8 +37,8 @@ actioncheck =
fwcmd_rich_rule = rule family='<family>' source address='<ip>' port port='$p' protocol='<protocol>' %(rich-suffix)s fwcmd_rich_rule = rule family='<family>' source address='<ip>' port port='$p' protocol='<protocol>' %(rich-suffix)s
actionban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done actionban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --add-rich-rule="%(fwcmd_rich_rule)s"; done
actionunban = ports="$(echo '<port>' | sed s/:/-/g)"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done actionunban = ports="<port>"; for p in $(echo $ports | tr ", " " "); do firewall-cmd --remove-rich-rule="%(fwcmd_rich_rule)s"; done
rich-suffix = <rich-blocktype> rich-suffix = <rich-blocktype>

View File

@ -4,52 +4,12 @@
# Modified: Yaroslav O. Halchenko <debian@onerussian.com> # Modified: Yaroslav O. Halchenko <debian@onerussian.com>
# made active on all ports from original iptables.conf # made active on all ports from original iptables.conf
# #
# # Obsolete: superseded by iptables[type=allports]
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
# Option: actionstart type = allports
# 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 = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -j f2b-<name>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<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 = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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 = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
[Init]

View File

@ -1,92 +0,0 @@
# Fail2Ban configuration file
#
# Author: Daniel Black
#
# This is a included configuration file and includes the definitions for the iptables
# used in all iptables based actions by default.
#
# The user can override the defaults in iptables-common.local
#
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# made config file IPv6 capable (see new section Init?family=inet6)
[INCLUDES]
after = iptables-blocktype.local
iptables-common.local
# iptables-blocktype.local is obsolete
[Definition]
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values: CMD
#
actionflush = <iptables> -F f2b-<name>
[Init]
# Option: chain
# Notes specifies the iptables chain to which the Fail2Ban rules should be
# added
# Values: STRING Default: INPUT
chain = INPUT
# Default name of the chain
#
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 | icmp | all ] Default: tcp
#
protocol = tcp
# Option: blocktype
# Note: This is what the action does with rules. This can be any jump target
# as per the iptables man page (section 8). Common values are DROP
# REJECT, REJECT --reject-with icmp-port-unreachable
# Values: STRING
blocktype = REJECT --reject-with icmp-port-unreachable
# Option: returntype
# Note: This is the default rule on "actionstart". This should be RETURN
# in all (blocking) actions, except REJECT in allowing actions.
# Values: STRING
returntype = RETURN
# Option: lockingopt
# Notes.: Option was introduced to iptables to prevent multiple instances from
# running concurrently and causing irratic behavior. -w was introduced
# in iptables 1.4.20, so might be absent on older systems
# See https://github.com/fail2ban/fail2ban/issues/1122
# Values: STRING
lockingopt = -w
# Option: iptables
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
iptables = iptables <lockingopt>
[Init?family=inet6]
# Option: blocktype (ipv6)
# Note: This is what the action does with rules. This can be any jump target
# as per the iptables man page (section 8). Common values are DROP
# REJECT, REJECT --reject-with icmp6-port-unreachable
# Values: STRING
blocktype = REJECT --reject-with icmp6-port-unreachable
# Option: iptables (ipv6)
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
iptables = ip6tables <lockingopt>

View File

@ -19,7 +19,7 @@
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
@ -28,7 +28,7 @@ before = iptables-common.conf
# Values: CMD # Values: CMD
# #
actionstart = ipset --create f2b-<name> iphash actionstart = ipset --create f2b-<name> iphash
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype> <_ipt_add_rules>
# Option: actionflush # Option: actionflush
@ -41,7 +41,7 @@ actionflush = ipset --flush f2b-<name>
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) # Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD # Values: CMD
# #
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype> actionstop = <_ipt_del_rules>
<actionflush> <actionflush>
ipset --destroy f2b-<name> ipset --destroy f2b-<name>
@ -61,5 +61,6 @@ actionban = ipset --test f2b-<name> <ip> || ipset --add f2b-<name> <ip>
# #
actionunban = ipset --test f2b-<name> <ip> && ipset --del f2b-<name> <ip> actionunban = ipset --test f2b-<name> <ip> && ipset --del f2b-<name> <ip>
[Init] # Several capabilities used internaly:
rule-jump = -m set --match-set f2b-<name> src -j <blocktype>

View File

@ -15,73 +15,13 @@
# #
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de> # Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# made config file IPv6 capable (see new section Init?family=inet6) # made config file IPv6 capable (see new section Init?family=inet6)
#
# Obsolete: superseded by iptables-ipset[type=allports]
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables-ipset.conf
[Definition] [Definition]
# Option: actionstart type = allports
# 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 <default-ipsettime> <familyopt>
<iptables> -I <chain> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values: CMD
#
actionflush = ipset flush <ipmset>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -m set --match-set <ipmset> src -j <blocktype>
<actionflush>
ipset destroy <ipmset>
# 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 = ipset add <ipmset> <ip> timeout <ipsettime> -exist
# actionprolong = %(actionban)s
# 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 = ipset del <ipmset> <ip> -exist
[Init]
# Option: default-ipsettime
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 0 (no timeout, managed by fail2ban by unban)
default-ipsettime = 0
# Option: ipsettime
# Notes: specifies ticket timeout (handled ipset timeout only)
# Values: [ NUM ] Default: 0 (managed by fail2ban by unban)
ipsettime = 0
# expresion to caclulate timeout from bantime, example:
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
ipmset = f2b-<name>
familyopt =
[Init?family=inet6]
ipmset = f2b-<name>6
familyopt = family inet6

View File

@ -15,73 +15,13 @@
# #
# Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de> # Modified: Alexander Koeppe <format_c@online.de>, Serg G. Brester <serg.brester@sebres.de>
# made config file IPv6 capable (see new section Init?family=inet6) # made config file IPv6 capable (see new section Init?family=inet6)
#
# Obsolete: superseded by iptables-ipset[type=multiport]
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables-ipset.conf
[Definition] [Definition]
# Option: actionstart type = multiport
# 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 <default-ipsettime> <familyopt>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values: CMD
#
actionflush = ipset flush <ipmset>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -m set --match-set <ipmset> src -j <blocktype>
<actionflush>
ipset destroy <ipmset>
# 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 = ipset add <ipmset> <ip> timeout <ipsettime> -exist
# actionprolong = %(actionban)s
# 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 = ipset del <ipmset> <ip> -exist
[Init]
# Option: default-ipsettime
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 0 (no timeout, managed by fail2ban by unban)
default-ipsettime = 0
# Option: ipsettime
# Notes: specifies ticket timeout (handled ipset timeout only)
# Values: [ NUM ] Default: 0 (managed by fail2ban by unban)
ipsettime = 0
# expresion to caclulate timeout from bantime, example:
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
ipmset = f2b-<name>
familyopt =
[Init?family=inet6]
ipmset = f2b-<name>6
familyopt = family inet6

View File

@ -0,0 +1,90 @@
# Fail2Ban configuration file
#
# Authors: Sergey G Brester (sebres), Daniel Black, Alexander Koeppe
#
# This is for ipset protocol 6 (and hopefully later) (ipset v6.14).
# Use ipset -V to see the protocol and version. Version 4 should use
# iptables-ipset-proto4.conf.
#
# This requires the program ipset which is normally in package called ipset.
#
# IPset was a feature introduced in the linux kernel 2.6.39 and 3.0.0 kernels.
#
# If you are running on an older kernel you make need to patch in external
# modules.
#
[INCLUDES]
before = iptables.conf
[Definition]
# 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 = ipset -exist create <ipmset> hash:ip timeout <default-ipsettime> <familyopt>
<_ipt_add_rules>
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values: CMD
#
actionflush = ipset flush <ipmset>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <_ipt_del_rules>
<actionflush>
ipset destroy <ipmset>
# 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 = ipset -exist add <ipmset> <ip> timeout <ipsettime>
# actionprolong = %(actionban)s
# 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 = ipset -exist del <ipmset> <ip>
# Several capabilities used internaly:
rule-jump = -m set --match-set <ipmset> src -j <blocktype>
[Init]
# Option: default-ipsettime
# Notes: specifies default timeout in seconds (handled default ipset timeout only)
# Values: [ NUM ] Default: 0 (no timeout, managed by fail2ban by unban)
default-ipsettime = 0
# Option: ipsettime
# Notes: specifies ticket timeout (handled ipset timeout only)
# Values: [ NUM ] Default: 0 (managed by fail2ban by unban)
ipsettime = 0
# expresion to caclulate timeout from bantime, example:
# banaction = %(known/banaction)s[ipsettime='<timeout-bantime>']
timeout-bantime = $([ "<bantime>" -le 2147483 ] && echo "<bantime>" || echo 0)
ipmset = f2b-<name>
familyopt =
[Init?family=inet6]
ipmset = f2b-<name>6
familyopt = family inet6

View File

@ -11,7 +11,7 @@
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]

View File

@ -3,50 +3,12 @@
# Author: Cyril Jaquier # Author: Cyril Jaquier
# Modified by Yaroslav Halchenko for multiport banning # Modified by Yaroslav Halchenko for multiport banning
# #
# Obsolete: superseded by iptables[type=multiport]
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
# Option: actionstart type = multiport
# 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 = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<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 = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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 = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
[Init]

View File

@ -4,51 +4,12 @@
# Copied from iptables.conf and modified by Yaroslav Halchenko # Copied from iptables.conf and modified by Yaroslav Halchenko
# to fulfill the needs of bugreporter dbts#350746. # to fulfill the needs of bugreporter dbts#350746.
# #
# # Obsolete: superseded by iptables[pre-rule='-m state --state NEW<sp>']
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
# Option: actionstart pre-rule = -m state --state NEW<sp>
# 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 = <iptables> -N f2b-<name>
<iptables> -A f2b-<name> -j <returntype>
<iptables> -I <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop = <iptables> -D <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
<actionflush>
<iptables> -X f2b-<name>
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<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 = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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 = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
[Init]

View File

@ -7,10 +7,14 @@
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
_ipt_chain_rule = -m recent --update --seconds 3600 --name <iptname> -j <blocktype>
_ipt_for_proto-iter =
_ipt_for_proto-done =
# Option: actionstart # 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). # 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 # Values: CMD
@ -33,7 +37,9 @@ before = iptables-common.conf
# own rules. The 3600 second timeout is independent and acts as a # own rules. The 3600 second timeout is independent and acts as a
# safeguard in case the fail2ban process dies unexpectedly. The # safeguard in case the fail2ban process dies unexpectedly. The
# shorter of the two timeouts actually matters. # shorter of the two timeouts actually matters.
actionstart = if [ `id -u` -eq 0 ];then <iptables> -I <chain> -m recent --update --seconds 3600 --name <iptname> -j <blocktype>;fi actionstart = if [ `id -u` -eq 0 ];then
{ %(_ipt_check_rule)s >/dev/null 2>&1; } || { <iptables> -I <chain> %(_ipt_chain_rule)s; }
fi
# Option: actionflush # Option: actionflush
# #
@ -46,13 +52,15 @@ actionflush =
# Values: CMD # Values: CMD
# #
actionstop = echo / > /proc/net/xt_recent/<iptname> actionstop = echo / > /proc/net/xt_recent/<iptname>
if [ `id -u` -eq 0 ];then <iptables> -D <chain> -m recent --update --seconds 3600 --name <iptname> -j <blocktype>;fi if [ `id -u` -eq 0 ];then
<iptables> -D <chain> %(_ipt_chain_rule)s;
fi
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed as invariant check (error by ban)
# Values: CMD # Values: CMD
# #
actioncheck = test -e /proc/net/xt_recent/<iptname> actioncheck = { <iptables> -C <chain> %(_ipt_chain_rule)s; } && test -e /proc/net/xt_recent/<iptname>
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the

View File

@ -1,28 +1,35 @@
# Fail2Ban configuration file # Fail2Ban configuration file
# #
# Author: Cyril Jaquier # Authors: Sergey G. Brester (sebres), Cyril Jaquier, Daniel Black,
# Yaroslav O. Halchenko, Alexander Koeppe et al.
# #
#
[INCLUDES]
before = iptables-common.conf
[Definition] [Definition]
# Option: type
# Notes.: type of the action.
# Values: [ oneport | multiport | allports ] Default: oneport
#
type = oneport
# Option: actionflush
# Notes.: command executed once to flush IPS, by shutdown (resp. by stop of the jail or this action)
# Values: CMD
#
actionflush = <iptables> -F f2b-<name>
# Option: actionstart # 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). # 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 # Values: CMD
# #
actionstart = <iptables> -N f2b-<name> actionstart = { <iptables> -C f2b-<name> -j <returntype> >/dev/null 2>&1; } || { <iptables> -N f2b-<name> || true; <iptables> -A f2b-<name> -j <returntype>; }
<iptables> -A f2b-<name> -j <returntype> <_ipt_add_rules>
<iptables> -I <chain> -p <protocol> --dport <port> -j f2b-<name>
# Option: actionstop # Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) # Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD # Values: CMD
# #
actionstop = <iptables> -D <chain> -p <protocol> --dport <port> -j f2b-<name> actionstop = <_ipt_del_rules>
<actionflush> <actionflush>
<iptables> -X f2b-<name> <iptables> -X f2b-<name>
@ -30,7 +37,7 @@ actionstop = <iptables> -D <chain> -p <protocol> --dport <port> -j f2b-<name>
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
# Values: CMD # Values: CMD
# #
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]' actioncheck = <_ipt_check_rules>
# Option: actionban # Option: actionban
# Notes.: command executed when banning an IP. Take care that the # Notes.: command executed when banning an IP. Take care that the
@ -48,5 +55,108 @@ actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
# #
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype> actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
# Option: pre-rule
# Notes.: prefix parameter(s) inserted to the begin of rule. No default (empty)
#
pre-rule =
rule-jump = -j <_ipt_rule_target>
# Several capabilities used internaly:
_ipt_for_proto-iter = for proto in $(echo '<protocol>' | sed 's/,/ /g'); do
_ipt_for_proto-done = done
_ipt_add_rules = <_ipt_for_proto-iter>
{ %(_ipt_check_rule)s >/dev/null 2>&1; } || { <iptables> -I <chain> %(_ipt_chain_rule)s; }
<_ipt_for_proto-done>
_ipt_del_rules = <_ipt_for_proto-iter>
<iptables> -D <chain> %(_ipt_chain_rule)s
<_ipt_for_proto-done>
_ipt_check_rules = <_ipt_for_proto-iter>
%(_ipt_check_rule)s
<_ipt_for_proto-done>
_ipt_chain_rule = <pre-rule><ipt_<type>/_chain_rule>
_ipt_check_rule = <iptables> -C <chain> %(_ipt_chain_rule)s
_ipt_rule_target = f2b-<name>
[ipt_oneport]
_chain_rule = -p $proto --dport <port> <rule-jump>
[ipt_multiport]
_chain_rule = -p $proto -m multiport --dports <port> <rule-jump>
[ipt_allports]
_chain_rule = -p $proto <rule-jump>
[Init] [Init]
# Option: chain
# Notes specifies the iptables chain to which the Fail2Ban rules should be
# added
# Values: STRING Default: INPUT
chain = INPUT
# Default name of the chain
#
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 | icmp | all ] Default: tcp
#
protocol = tcp
# Option: blocktype
# Note: This is what the action does with rules. This can be any jump target
# as per the iptables man page (section 8). Common values are DROP
# REJECT, REJECT --reject-with icmp-port-unreachable
# Values: STRING
blocktype = REJECT --reject-with icmp-port-unreachable
# Option: returntype
# Note: This is the default rule on "actionstart". This should be RETURN
# in all (blocking) actions, except REJECT in allowing actions.
# Values: STRING
returntype = RETURN
# Option: lockingopt
# Notes.: Option was introduced to iptables to prevent multiple instances from
# running concurrently and causing irratic behavior. -w was introduced
# in iptables 1.4.20, so might be absent on older systems
# See https://github.com/fail2ban/fail2ban/issues/1122
# Values: STRING
lockingopt = -w
# Option: iptables
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
iptables = iptables <lockingopt>
[Init?family=inet6]
# Option: blocktype (ipv6)
# Note: This is what the action does with rules. This can be any jump target
# as per the iptables man page (section 8). Common values are DROP
# REJECT, REJECT --reject-with icmp6-port-unreachable
# Values: STRING
blocktype = REJECT --reject-with icmp6-port-unreachable
# Option: iptables (ipv6)
# Notes.: Actual command to be executed, including common to all calls options
# Values: STRING
iptables = ip6tables <lockingopt>

View File

@ -0,0 +1,107 @@
# IPThreat configuration file
#
# Added to fail2ban by Jeff Johnson (jjxtra)
#
# Action to report IP address to ipthreat.net
#
# You must sign up to obtain an API key from ipthreat.net and request bulk report permissions
# https://ipthreat.net/integrations
#
# IPThreat is a 100% free site and service, all data is licensed under a creative commons by attribution license
# Please do not integrate if you do not agree to the license
#
# IMPORTANT:
#
# Reporting an IP is a serious action. Make sure that it is legit.
# Consider using this action only for:
# * IP that has been banned more than once
# * High max retry to avoid user mis-typing password
# * Filters that are unlikely to be human error
#
# Example:
# ```
# action = %(known/action)s
# ipthreat[]
# ```
#
# The action accepts the following arguments: ipthreat[ipthreat_flags="8",ipthreat_system="SSH", ipthreat_apikey=...]
# In most cases your action could be as simple as: ipthreat[], since the default flags and system are set to the most correct default values.
# You can optionally override ipthreat_system and ipthreat_flags if desired.
# The ipthreat_apikey must be set at the bottom of this configuration file.
#
# `ipthreat_system` is a short name of the system attacked, i.e. SSH, SMTP, MYSQL, PHP, etc.
#
# For `ipthreat_flags`, most cases will use 8 (BruteForce) which is the default, but you could use others.
# You can use the name or the ordinal.
# Multiple values are comma separated.
# ```
# Name Ordinal Description
# Dns 1 Abuse/attack of dns (domain name server)
# Fraud 2 General fraud, whether orders, misuse of payment info, etc
# DDos 4 Distributed denial of service attack, whether through http requests, large ping attack, etc
# BruteForce 8 Brute force login attack
# Proxy 16 IP is a proxy like TOR or other proxy server
# Spam 32 Email, comment or other type of spam
# Vpn 64 IP is part of a VPN
# Hacking 128 General hacking outside of brute force attack (includes vulnerability scans, sql injection, etc.). Use port scan flag instead if it's just probe on ports.
# BadBot 256 Bad bot that is not honoring robots.txt or just flooding with too many requests, etc
# Compromised 512 The ip has been taken over by malware or botnet
# Phishing 1024 The ip is involved in phishing or spoofing
# Iot 2048 The ip has targetted an iot (Internet of Things) device
# PortScan 4096 Port scan
# See https://ipthreat.net/bulkreportformat for more information
# ```
[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
#
actionstart =
# Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
# Values: CMD
#
actionstop =
# Option: actioncheck
# Notes.: command executed once before each actionban command
# Values: CMD
#
actioncheck =
# 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 = curl -sSf "https://api.ipthreat.net/api/report" -X POST -H "Content-Type: application/json" -H "X-API-KEY: <ipthreat_apikey>" -d "{\"ip\":\"<ip>\",\"flags\":\"<ipthreat_flags>\",\"system\":\"<ipthreat_system>\",\"notes\":\"fail2ban\"}"
# 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 =
[Init]
# Option: ipthreat_apikey
# Notes Your API key from ipthreat.net
# Values: STRING Default: None
# Register for ipthreat [https://ipthreat.net], get api key and set below.
# You will need to set the flags and system in the action call in jail.conf
ipthreat_apikey =
# By default, the ipthreat system is the name of the fail2ban jail
ipthreat_system = <name>
# By default the ip threat flags is 8 (brute force), but you can override this per jail if desired
ipthreat_flags = 8

View File

@ -17,7 +17,7 @@ actionstart = printf %%b "Hi,\n
The jail <name> has been started successfully.\n The jail <name> has been started successfully.\n
Output will be buffered until <lines> lines are available.\n Output will be buffered until <lines> lines are available.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
# Option: actionstop # Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) # Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
@ -28,13 +28,13 @@ actionstop = if [ -f <tmpfile> ]; then
These hosts have been banned by Fail2Ban.\n These hosts have been banned by Fail2Ban.\n
`cat <tmpfile>` `cat <tmpfile>`
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary from <fq-hostname>" <dest>
rm <tmpfile> rm <tmpfile>
fi fi
printf %%b "Hi,\n printf %%b "Hi,\n
The jail <name> has been stopped.\n The jail <name> has been stopped.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
@ -55,7 +55,7 @@ actionban = printf %%b "`date`: <ip> (<failures> failures)\n" >> <tmpfile>
These hosts have been banned by Fail2Ban.\n These hosts have been banned by Fail2Ban.\n
`cat <tmpfile>` `cat <tmpfile>`
\nRegards,\n \nRegards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: Summary" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: Summary" <dest>
rm <tmpfile> rm <tmpfile>
fi fi

View File

@ -72,7 +72,7 @@ actionunban =
# Notes.: Your system mail command. Is passed 2 args: subject and recipient # Notes.: Your system mail command. Is passed 2 args: subject and recipient
# Values: CMD # Values: CMD
# #
mailcmd = mail -s mailcmd = mail -E 'set escape' -s
# Default name of the chain # Default name of the chain
# #

View File

@ -20,7 +20,7 @@ norestored = 1
actionstart = printf %%b "Hi,\n actionstart = printf %%b "Hi,\n
The jail <name> has been started successfully.\n The jail <name> has been started successfully.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
# Option: actionstop # Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) # Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
@ -29,7 +29,7 @@ actionstart = printf %%b "Hi,\n
actionstop = printf %%b "Hi,\n actionstop = printf %%b "Hi,\n
The jail <name> has been stopped.\n The jail <name> has been stopped.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
@ -49,7 +49,7 @@ actionban = printf %%b "Hi,\n
Here is more information about <ip> :\n Here is more information about <ip> :\n
`%(_whois_command)s`\n `%(_whois_command)s`\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the

View File

@ -16,7 +16,7 @@ norestored = 1
actionstart = printf %%b "Hi,\n actionstart = printf %%b "Hi,\n
The jail <name> has been started successfully.\n The jail <name> has been started successfully.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: started on <fq-hostname>" <dest>
# Option: actionstop # Option: actionstop
# Notes.: command executed at the stop of jail (or at the end of Fail2Ban) # Notes.: command executed at the stop of jail (or at the end of Fail2Ban)
@ -25,7 +25,7 @@ actionstart = printf %%b "Hi,\n
actionstop = printf %%b "Hi,\n actionstop = printf %%b "Hi,\n
The jail <name> has been stopped.\n The jail <name> has been stopped.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: stopped on <fq-hostname>" <dest>
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
@ -43,7 +43,7 @@ actionban = printf %%b "Hi,\n
The IP <ip> has just been banned by Fail2Ban after The IP <ip> has just been banned by Fail2Ban after
<failures> attempts against <name>.\n <failures> attempts against <name>.\n
Regards,\n Regards,\n
Fail2Ban"|mail -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest> Fail2Ban"|mail -E 'set escape' -s "[Fail2Ban] <name>: banned <ip> from <fq-hostname>" <dest>
# Option: actionunban # Option: actionunban
# Notes.: command executed when unbanning an IP. Take care that the # Notes.: command executed when unbanning an IP. Take care that the

View File

@ -84,8 +84,15 @@ srv_cfg_path = /etc/nginx/
#srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf #srv_cmd = nginx -c %(srv_cfg_path)s/nginx.conf
srv_cmd = nginx srv_cmd = nginx
# first test configuration is correct, hereafter send reload signal: # pid file (used to check nginx is running):
blck_lst_reload = %(srv_cmd)s -qt; if [ $? -eq 0 ]; then srv_pid = /run/nginx.pid
# command used to check whether nginx is running and configuration is valid:
srv_is_running = [ -f "%(srv_pid)s" ]
srv_check_cmd = %(srv_is_running)s && %(srv_cmd)s -qt
# first test nginx is running and configuration is correct, hereafter send reload signal:
blck_lst_reload = %(srv_check_cmd)s; if [ $? -eq 0 ]; then
%(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi; %(srv_cmd)s -s reload; if [ $? -ne 0 ]; then echo 'reload failed.'; fi;
fi; fi;

View File

@ -5,7 +5,7 @@
[INCLUDES] [INCLUDES]
before = iptables-common.conf before = iptables.conf
[Definition] [Definition]
@ -41,6 +41,11 @@ actionban = echo 'all' >| /etc/symbiosis/firewall/blacklist.d/<ip>.auto
actionunban = rm -f /etc/symbiosis/firewall/blacklist.d/<ip>.auto actionunban = rm -f /etc/symbiosis/firewall/blacklist.d/<ip>.auto
<iptables> -D <chain> -s <ip> -j <blocktype> || : <iptables> -D <chain> -s <ip> -j <blocktype> || :
# [TODO] Flushing is currently not implemented for symbiosis blacklist.d
#
actionflush =
[Init] [Init]
# Option: chain # Option: chain

View File

@ -13,16 +13,45 @@ actionstop =
actioncheck = actioncheck =
actionban = [ -n "<application>" ] && app="app <application>" # ufw does "quickly process packets for which we already have a connection" in before.rules,
ufw insert <insertpos> <blocktype> from <ip> to <destination> $app # therefore all related sockets should be closed
# actionban is using `ss` to do so, this only handles IPv4 and IPv6.
actionunban = [ -n "<application>" ] && app="app <application>" actionban = if [ -n "<application>" ] && ufw app info "<application>"
ufw delete <blocktype> from <ip> to <destination> $app then
ufw <add> <blocktype> from <ip> to <destination> app "<application>" comment "<comment>"
else
ufw <add> <blocktype> from <ip> to <destination> comment "<comment>"
fi
<kill>
actionunban = if [ -n "<application>" ] && ufw app info "<application>"
then
ufw delete <blocktype> from <ip> to <destination> app "<application>"
else
ufw delete <blocktype> from <ip> to <destination>
fi
# Option: kill-mode
# Notes.: can be set to ss or conntrack (may be extended later with other modes) to immediately drop all connections from banned IP, default empty (no kill)
# Example: banaction = ufw[kill-mode=ss]
kill-mode =
# intern conditional parameter used to provide killing mode after ban:
_kill_ =
_kill_ss = ss -K dst "[<ip>]"
_kill_conntrack = conntrack -D -s "<ip>"
# Option: kill
# Notes.: can be used to specify custom killing feature, by default depending on option kill-mode
# Examples: banaction = ufw[kill='ss -K "( sport = :http || sport = :https )" dst "[<ip>]"']
# banaction = ufw[kill='cutter "<ip>"']
kill = <_kill_<kill-mode>>
[Init] [Init]
# Option: insertpos # Option: add
# Notes.: The position number in the firewall list to insert the block rule # Notes.: can be set to "insert 1" to insert a rule at certain position (here 1):
insertpos = 1 add = prepend
# Option: blocktype # Option: blocktype
# Notes.: reject or deny # Notes.: reject or deny
@ -36,6 +65,10 @@ destination = any
# Notes.: application from sudo ufw app list # Notes.: application from sudo ufw app list
application = application =
# Option: comment
# Notes.: comment for rule added by fail2ban
comment = by Fail2Ban after <failures> attempts against <name>
# DEV NOTES: # DEV NOTES:
# #
# Author: Guilhem Lettron # Author: Guilhem Lettron

View File

@ -24,13 +24,13 @@
loglevel = INFO loglevel = INFO
# Option: logtarget # Option: logtarget
# Notes.: Set the log target. This could be a file, SYSLOG, STDERR or STDOUT. # Notes.: Set the log target. This could be a file, SYSTEMD-JOURNAL, SYSLOG, STDERR or STDOUT.
# Only one log target can be specified. # Only one log target can be specified.
# If you change logtarget from the default value and you are # If you change logtarget from the default value and you are
# using logrotate -- also adjust or disable rotation in the # using logrotate -- also adjust or disable rotation in the
# corresponding configuration file # corresponding configuration file
# (e.g. /etc/logrotate.d/fail2ban on Debian systems) # (e.g. /etc/logrotate.d/fail2ban on Debian systems)
# Values: [ STDOUT | STDERR | SYSLOG | SYSOUT | FILE ] Default: STDERR # Values: [ STDOUT | STDERR | SYSLOG | SYSOUT | SYSTEMD-JOURNAL | FILE ] Default: STDERR
# #
logtarget = /var/log/fail2ban.log logtarget = /var/log/fail2ban.log
@ -55,6 +55,12 @@ socket = /var/run/fail2ban/fail2ban.sock
# #
pidfile = /var/run/fail2ban/fail2ban.pid pidfile = /var/run/fail2ban/fail2ban.pid
# Option: allowipv6
# Notes.: Allows IPv6 interface:
# Default: auto
# Values: [ auto yes (on, true, 1) no (off, false, 0) ] Default: auto
#allowipv6 = auto
# Options: dbfile # Options: dbfile
# Notes.: Set the file for the fail2ban persistent data to be stored. # Notes.: Set the file for the fail2ban persistent data to be stored.
# A value of ":memory:" means database is only stored in memory # A value of ":memory:" means database is only stored in memory

View File

@ -2,11 +2,11 @@
[Definition] [Definition]
failregex = ^<HOST> .*Googlebot.*$ failregex = ^\s*<HOST> \S+ \S+(?: \S+)?\s+\S+ "[A-Z]+ /\S* [^"]*" \d+ \d+ \"[^"]*\" "[^"]*\bGooglebot/[^"]*"
ignoreregex = ignoreregex =
datepattern = ^[^\[]*\[({DATE}) datepattern = ^[^\[]*(\[{DATE}\s*\])
{^LN-BEG} {^LN-BEG}
# DEV Notes: # DEV Notes:

View File

@ -8,7 +8,7 @@ before = apache-common.conf
[Definition] [Definition]
failregex = ^%(_apache_error_client)s (?:(?:AH0013[456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b) failregex = ^%(_apache_error_client)s (?:(?:AH001[23][456]: )?Invalid (method|URI) in request\b|(?:AH00565: )?request failed: URI too long \(longer than \d+\)|request failed: erroneous characters after protocol string:|(?:AH00566: )?request failed: invalid characters in URI\b)
ignoreregex = ignoreregex =

View File

@ -21,7 +21,7 @@ log_prefix= (?:NOTICE|SECURITY|WARNING)%(__pid_re)s:?(?:\[C-[\da-f]*\])?:? [^:]+
prefregex = ^%(__prefix_line)s%(log_prefix)s <F-CONTENT>.+</F-CONTENT>$ 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)$ 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 ^Call from '[^']*' \((?:(?:TCP|UDP):)?<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) ^(?:Host )?<HOST> (?:failed (?:to authenticate\b|MD5 authentication\b)|tried to authenticate with nonexistent user\b)
^No registration for peer '[^']*' \(from <HOST>\)$ ^No registration for peer '[^']*' \(from <HOST>\)$
^hacking attempt detected '<HOST>'$ ^hacking attempt detected '<HOST>'$

View File

@ -10,7 +10,7 @@ after = common.local
[DEFAULT] [DEFAULT]
# Type of log-file resp. log-format (file, short, journal, rfc542): # Type of log-file resp. log-format (file, short, journal, rfc5424):
logtype = file logtype = file
# Daemon definition is to be specialized (if needed) in .conf file # Daemon definition is to be specialized (if needed) in .conf file

View File

@ -11,7 +11,7 @@ before = common.conf
_daemon = (?:courier)?(?:imapd?|pop3d?)(?:login)?(?:-ssl)? _daemon = (?:courier)?(?:imapd?|pop3d?)(?:login)?(?:-ssl)?
failregex = ^%(__prefix_line)sLOGIN FAILED, (?:user|method)=.*, ip=\[<HOST>\]$ failregex = ^%(__prefix_line)sLOGIN FAILED, (?:(?!ip=)(?:user=<F-USER>[^,]*</F-USER>|\w+=[^,]*), )*ip=\[<HOST>\]
ignoreregex = ignoreregex =

View File

@ -8,14 +8,15 @@ before = common.conf
[Definition] [Definition]
_auth_worker = (?:dovecot: )?auth(?:-worker)? _auth_worker = (?:dovecot: )?auth(?:-worker)?
_auth_worker_info = (?:conn \w+:auth(?:-worker)? \([^\)]+\): auth(?:-worker)?<\d+>: )?
_daemon = (?:dovecot(?:-auth)?|auth) _daemon = (?:dovecot(?:-auth)?|auth)
prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?<F-CONTENT>.+</F-CONTENT>$ prefregex = ^%(__prefix_line)s(?:%(_auth_worker)s(?:\([^\)]+\))?: )?(?:%(__pam_auth)s(?:\(dovecot:auth\))?: |(?:pop3|imap|managesieve|submission)-login: )?(?:Info: )?%(_auth_worker_info)s<F-CONTENT>.+</F-CONTENT>$
failregex = ^authentication failure; logname=<F-ALT_USER1>\S*</F-ALT_USER1> uid=\S* euid=\S* tty=dovecot ruser=<F-USER>\S*</F-USER> rhost=<HOST>(?:\s+user=<F-ALT_USER>\S*</F-ALT_USER>)?\s*$ failregex = ^authentication failure; logname=<F-ALT_USER1>\S*</F-ALT_USER1> uid=\S* euid=\S* tty=dovecot ruser=<F-USER>\S*</F-USER> rhost=<HOST>(?:\s+user=<F-ALT_USER>\S*</F-ALT_USER>)?\s*$
^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:auth failed, \d+ attempts(?: in \d+ secs)?|tried to use (?:disabled|disallowed) \S+ auth|proxy dest auth failed)\):(?: user=<<F-USER>[^>]*</F-USER>>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$ ^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?:: (?:[^\(]+|\w+\([^\)]*\))+)? \((?:auth failed, \d+ attempts(?: in \d+ secs)?|tried to use (?:disabled|disallowed) \S+ auth|proxy dest auth failed)\):(?: user=<<F-USER>[^>]*</F-USER>>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$
^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \(password mismatch\?\)|Permission denied)\s*$ ^pam\(\S+,<HOST>(?:,\S*)?\): pam_authenticate\(\) failed: (?:User not known to the underlying authentication module: \d+ Time\(s\)|Authentication failure \([Pp]assword mismatch\?\)|Permission denied)\s*$
^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:unknown user|invalid credentials|Password mismatch) ^[a-z\-]{3,15}\(\S*,<HOST>(?:,\S*)?\): (?:[Uu]nknown user|[Ii]nvalid credentials|[Pp]assword mismatch)
<mdre-<mode>> <mdre-<mode>>
mdre-aggressive = ^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:no auth attempts|disconnected before auth was ready,|client didn't finish \S+ auth,)(?: (?:in|waited) \d+ secs)?\):(?: user=<[^>]*>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$ mdre-aggressive = ^(?:Aborted login|Disconnected|Remote closed connection|Client has quit the connection)(?::(?: [^ \(]+)+)? \((?:no auth attempts|disconnected before auth was ready,|client didn't finish \S+ auth,)(?: (?:in|waited) \d+ secs)?\):(?: user=<[^>]*>,)?(?: method=\S+,)? rip=<HOST>(?:[^>]*(?:, session=<\S+>)?)\s*$

View File

@ -14,7 +14,7 @@ before = common.conf
[Definition] [Definition]
failregex = ^%(__prefix_line)s(https?:\/\/)([\da-z\.-]+)\.([a-z\.]{2,6})(\/[\w\.-]+)*\|\d{10}\|user\|<HOST>\|.+\|.+\|\d\|.*\|Login attempt failed for .+\.$ failregex = ^%(__prefix_line)s(?:https?:\/\/)[^|]+\|[^|]+\|[^|]+\|<ADDR>\|(?:[^|]*\|)*Login attempt failed (?:for|from) <F-USER>[^|]+</F-USER>\.$
ignoreregex = ignoreregex =

View File

@ -12,7 +12,7 @@ after = exim-common.local
host_info_pre = (?:H=([\w.-]+ )?(?:\(\S+\) )?)? host_info_pre = (?:H=([\w.-]+ )?(?:\(\S+\) )?)?
host_info_suf = (?::\d+)?(?: I=\[\S+\](:\d+)?)?(?: U=\S+)?(?: P=e?smtp)?(?: F=(?:<>|[^@]+@\S+))?\s host_info_suf = (?::\d+)?(?: I=\[\S+\](:\d+)?)?(?: U=\S+)?(?: P=e?smtp)?(?: F=(?:<>|[^@]+@\S+))?\s
host_info = %(host_info_pre)s\[<HOST>\]%(host_info_suf)s host_info = %(host_info_pre)s\[<HOST>\]%(host_info_suf)s
pid = (?: \[\d+\])? pid = (?: \[\d+\]| \w+ exim\[\d+\]:)?
# DEV Notes: # DEV Notes:
# From exim source code: ./src/receive.c:add_host_info_for_log # From exim source code: ./src/receive.c:add_host_info_for_log

View File

@ -6,24 +6,35 @@
# #
import sys import sys
from fail2ban.server.ipdns import DNSUtils, IPAddr from fail2ban.server.ipdns import DNSUtils, IPAddr
from threading import Thread
def process_args(argv): def process_args(argv):
if len(argv) != 2: if len(argv) - 1 not in (1, 2):
raise ValueError("Please provide a single IP as an argument. Got: %s\n" raise ValueError("Usage %s ip ?timeout?. Got: %s\n"
% (argv[1:])) % (argv[0], argv[1:]))
ip = argv[1] ip = argv[1]
if not IPAddr(ip).isValid: if not IPAddr(ip).isValid:
raise ValueError("Argument must be a single valid IP. Got: %s\n" raise ValueError("Argument must be a single valid IP. Got: %s\n"
% ip) % ip)
return ip return argv[1:]
google_ips = None google_ips = None
def is_googlebot(ip): def is_googlebot(ip, timeout=55):
import re import re
timeout = float(timeout or 0)
if timeout:
def ipToNameTO(host, ip, timeout):
host[0] = DNSUtils.ipToName(ip)
host = [None]
th = Thread(target=ipToNameTO, args=(host, ip, timeout)); th.daemon=True; th.start()
th.join(timeout)
host = host[0]
else:
host = DNSUtils.ipToName(ip) host = DNSUtils.ipToName(ip)
if not host or not re.match(r'.*\.google(bot)?\.com$', host): if not host or not re.match(r'.*\.google(bot)?\.com$', host):
return False return False
host_ips = DNSUtils.dnsToIp(host) host_ips = DNSUtils.dnsToIp(host)
@ -31,7 +42,7 @@ def is_googlebot(ip):
if __name__ == '__main__': # pragma: no cover if __name__ == '__main__': # pragma: no cover
try: try:
ret = is_googlebot(process_args(sys.argv)) ret = is_googlebot(*process_args(sys.argv))
except ValueError as e: except ValueError as e:
sys.stderr.write(str(e)) sys.stderr.write(str(e))
sys.exit(2) sys.exit(2)

View File

@ -3,7 +3,7 @@
[Definition] [Definition]
failregex = ^: \((?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match .* username: .*|digest: auth failed for .*: wrong password|get_password failed), IP: <HOST>\s*$ failregex = ^\s*(?:: )?\(?(?:http|mod)_auth\.c\.\d+\) (?:password doesn\'t match for (?:\S+|.*?) username:\s+<F-USER>(?:\S+|.*?)</F-USER>\s*|digest: auth failed(?: for\s+<F-ALT_USER>(?:\S+|.*?)</F-ALT_USER>\s*)?: (?:wrong password|uri mismatch \([^\)]*\))|get_password failed),? IP: <HOST>\s*$
ignoreregex = ignoreregex =

View File

@ -0,0 +1,25 @@
# Fail2Ban filter for Monitorix (HTTP built-in server)
#
[INCLUDES]
before = common.conf
[Definition]
_daemon = monitorix-httpd
# Option: failregex
# Notes.: regex to match the password failures messages in the logfile. The
# host must be matched by a group named "host". The tag "<HOST>" can
# be used for standard IP/hostname matching and is only an alias for
# (?:::f{4,6}:)?(?P<host>\S+)
# Values: TEXT
#
failregex = ^(?:\s+-)?\s*(?:NOTEXIST|AUTHERR|NOTALLOWED) - <ADDR>\b
# Option: ignoreregex
# Notes.: regex to ignore. If this regex matches, the line is ignored.
# Values: TEXT
#
ignoreregex =

View File

@ -0,0 +1,15 @@
# Fail2Ban filter for failed MSSQL Server authentication attempts
[Definition]
failregex = ^\s*Logon\s+Login failed for user '<F-USER>(?:[^']*|.*)</F-USER>'\. [^'\[]+\[CLIENT: <ADDR>\]$
# DEV Notes:
# Tested with SQL Server 2019 on Ubuntu 18.04
#
# Example:
# 2020-02-24 14:48:55.12 Logon Login failed for user 'root'. Reason: Could not find a login matching the name provided. [CLIENT: 127.0.0.1]
#
# Author: Rüdiger Olschewsky
#

View File

@ -22,7 +22,7 @@
[Definition] [Definition]
# Daemon name # Daemon name
_daemon=named _daemon=named(?:-\w+)?
# Shortcuts for easier comprehension of the failregex # Shortcuts for easier comprehension of the failregex
@ -32,7 +32,7 @@ __daemon_combs_re=(?:%(__pid_re)s?:\s+%(__daemon_re)s|%(__daemon_re)s%(__pid_re)
# hostname daemon_id spaces # hostname daemon_id spaces
# this can be optional (for instance if we match named native log files) # this can be optional (for instance if we match named native log files)
__line_prefix=(?:\s\S+ %(__daemon_combs_re)s\s+)? __line_prefix=(?:\s*\S+ %(__daemon_combs_re)s\s+)?
prefregex = ^%(__line_prefix)s(?: error:)?\s*client(?: @\S*)? <HOST>#\S+(?: \([\S.]+\))?: <F-CONTENT>.+</F-CONTENT>\s(?:denied|\(NOTAUTH\))\s*$ prefregex = ^%(__line_prefix)s(?: error:)?\s*client(?: @\S*)? <HOST>#\S+(?: \([\S.]+\))?: <F-CONTENT>.+</F-CONTENT>\s(?:denied|\(NOTAUTH\))\s*$

View File

@ -0,0 +1,16 @@
# Fail2Ban filter to match bad requests to nginx
#
[Definition]
# The request often doesn't contain a method, only some encoded garbage
# This will also match requests that are entirely empty
failregex = ^<HOST> - \S+ \[\] "[^"]*" 400
datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]%%f)?(?:\s*%%z)?
^[^\[]*\[({DATE})
{^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
# Author: Jan Przybylak

View File

@ -17,6 +17,8 @@ datepattern = {^LN-BEG}%%ExY(?P<_sep>[-/.])%%m(?P=_sep)%%d[T ]%%H:%%M:%%S(?:[.,]
^[^\[]*\[({DATE}) ^[^\[]*\[({DATE})
{^LN-BEG} {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
# DEV Notes: # DEV Notes:
# Based on apache-botsearch filter # Based on apache-botsearch filter
# #

View File

@ -3,15 +3,32 @@
[Definition] [Definition]
mode = normal
failregex = ^ \[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$ mdre-auth = ^\s*\[error\] \d+#\d+: \*\d+ user "(?:[^"]+|.*?)":? (?:password mismatch|was not found in "[^\"]*"), client: <HOST>, server: \S*, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"(?:, referrer: "\S+")?\s*$
mdre-fallback = ^\s*\[crit\] \d+#\d+: \*\d+ SSL_do_handshake\(\) failed \(SSL: error:\S+(?: \S+){1,3} too (?:long|short)\)[^,]*, client: <HOST>
mdre-normal = %(mdre-auth)s
mdre-aggressive = %(mdre-auth)s
%(mdre-fallback)s
failregex = <mdre-<mode>>
ignoreregex = ignoreregex =
datepattern = {^LN-BEG} datepattern = {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx
# DEV NOTES: # DEV NOTES:
# mdre-auth:
# Based on samples in https://github.com/fail2ban/fail2ban/pull/43/files # Based on samples in https://github.com/fail2ban/fail2ban/pull/43/files
# Extensive search of all nginx auth failures not done yet. # Extensive search of all nginx auth failures not done yet.
# #
# Author: Daniel Black # Author: Daniel Black
# mdre-fallback:
# Ban people checking for TLS_FALLBACK_SCSV repeatedly
# https://stackoverflow.com/questions/28010492/nginx-critical-error-with-ssl-handshaking/28010608#28010608
# Author: Stephan Orlowsky

View File

@ -44,3 +44,6 @@ failregex = ^\s*\[[a-z]+\] \d+#\d+: \*\d+ limiting requests, excess: [\d\.]+ by
ignoreregex = ignoreregex =
datepattern = {^LN-BEG} datepattern = {^LN-BEG}
journalmatch = _SYSTEMD_UNIT=nginx.service + _COMM=nginx

View File

@ -22,8 +22,8 @@ _daemon = nsd
# (?:::f{4,6}:)?(?P<host>[\w\-.^_]+) # (?:::f{4,6}:)?(?P<host>[\w\-.^_]+)
# Values: TEXT # Values: TEXT
failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <HOST> TYPE255$ failregex = ^%(__prefix_line)sinfo: ratelimit block .* query <ADDR> TYPE255$
^%(__prefix_line)sinfo: .* <HOST> refused, no acl matches\.$ ^%(__prefix_line)sinfo: .* from(?: client)? <ADDR> refused, no acl matches\.?$
ignoreregex = ignoreregex =

View File

@ -12,16 +12,15 @@ before = common.conf
_daemon = postfix(-\w+)?/\w+(?:/smtp[ds])? _daemon = postfix(-\w+)?/\w+(?:/smtp[ds])?
_port = (?::\d+)? _port = (?::\d+)?
_pref = [A-Z]{4}
prefregex = ^%(__prefix_line)s<mdpr-<mode>> <F-CONTENT>.+</F-CONTENT>$ prefregex = ^%(__prefix_line)s<mdpr-<mode>> <F-CONTENT>.+</F-CONTENT>$
mdpr-normal = (?:\w+: reject:|(?:improper command pipelining|too many errors) after \S+) # Extended RE for normal mode to match reject by unknown users or undeliverable address, can be set to empty to avoid this:
mdre-normal=^RCPT from [^[]*\[<HOST>\]%(_port)s: 55[04] 5\.7\.1\s exre-user = |[Uu](?:ser unknown|ndeliverable address)
^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 mdpr-normal = (?:\w+: (?:milter-)?reject:|(?:improper command pipelining|too many errors) after \S+)
^EHLO from [^[]*\[<HOST>\]%(_port)s: 504 5\.5\.\d+ (<[^>]*>)?: Helo command rejected: need fully-qualified hostname\b mdre-normal=^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45][50][04] [45]\.\d\.\d+ (?:(?:<[^>]*>)?: )?(?:(?:Helo command|(?:Sender|Recipient) address) rejected: )?(?:Service unavailable|(?:Client host|Command|Data command) rejected|Relay access denied|(?:Host|Domain) not found|need fully-qualified hostname|match%(exre-user)s)\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:? ^from [^[]*\[<HOST>\]%(_port)s:?
mdpr-auth = warning: mdpr-auth = warning:
@ -31,13 +30,15 @@ mdre-auth2= ^[^[]*\[<HOST>\]%(_port)s: SASL ((?i)LOGIN|PLAIN|(?:CRAM|DIGEST)-MD5
# Mode "rbl" currently included in mode "normal", but if needed for jail "postfix-rbl" only: # Mode "rbl" currently included in mode "normal", but if needed for jail "postfix-rbl" only:
mdpr-rbl = %(mdpr-normal)s mdpr-rbl = %(mdpr-normal)s
mdre-rbl = ^RCPT from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b mdre-rbl = ^%(_pref)s from [^[]*\[<HOST>\]%(_port)s: [45]54 [45]\.7\.1 Service unavailable; Client host \[\S+\] blocked\b
# Mode "rbl" currently included in mode "normal" (within 1st rule) # Mode "rbl" currently included in mode "normal" (within 1st rule)
mdpr-more = %(mdpr-normal)s mdpr-more = %(mdpr-normal)s
mdre-more = %(mdre-normal)s mdre-more = %(mdre-normal)s
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+))) # Includes some of the log messages described in
# <http://www.postfix.org/POSTSCREEN_README.html>.
mdpr-ddos = (?:lost connection after(?! DATA) [A-Z]+|disconnect(?= from \S+(?: \S+=\d+)* auth=0/(?:[1-9]|\d\d+))|(?:PREGREET \d+|HANGUP) after \S+|COMMAND (?:TIME|COUNT|LENGTH) LIMIT)
mdre-ddos = ^from [^[]*\[<HOST>\]%(_port)s:? mdre-ddos = ^from [^[]*\[<HOST>\]%(_port)s:?
mdpr-extra = (?:%(mdpr-auth)s|%(mdpr-normal)s) mdpr-extra = (?:%(mdpr-auth)s|%(mdpr-normal)s)

View File

@ -0,0 +1,17 @@
# Fail2Ban filter for port scans detected by scanlogd
[INCLUDES]
# Read common prefixes. If any customizations available -- read them from
# common.local
before = common.conf
[Definition]
_daemon = scanlogd
failregex = ^%(__prefix_line)s<ADDR>(?::<F-PORT/>)? to \S+ ports\b
ignoreregex =
# Author: Mike Gabriel <mike.gabriel@das-netzwerkteam.de>

View File

@ -15,7 +15,7 @@ addr = (?:IPv6:<IP6>|<IP4>)
prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$ prefregex = ^<F-MLFID>%(__prefix_line)s</F-MLFID><F-CONTENT>.+</F-CONTENT>$
failregex = ^(\S+ )?\[%(addr)s\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$ failregex = ^(\S+ )?\[%(addr)s\]( \(may be forged\))?: possible SMTP attack: command=AUTH, count=\d+$
^AUTH failure \(LOGIN\):(?: [^:]+:)? authentication failure: checkpass failed, user=<F-USER>(?:\S+|.*?)</F-USER>, relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$ ^AUTH failure \([^\)]+\):(?: [^:]+:)? (?:authentication failure|user not found): [^,]*, (?:user=<F-USER>(?:\S+|.*?)</F-USER>, )?relay=(?:\S+ )?\[%(addr)s\](?: \(may be forged\))?$
ignoreregex = ignoreregex =
journalmatch = _SYSTEMD_UNIT=sendmail.service journalmatch = _SYSTEMD_UNIT=sendmail.service

View File

@ -21,12 +21,12 @@ before = common.conf
_daemon = (?:(sm-(mta|acceptingconnections)|sendmail)) _daemon = (?:(sm-(mta|acceptingconnections)|sendmail))
__prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )? __prefix_line = %(known/__prefix_line)s(?:\w{14,20}: )?
addr = (?:IPv6:<IP6>|<IP4>) addr = (?:(?:IPv6:)?<IP6>|<IP4>)
prefregex = ^<F-MLFID>%(__prefix_line)s</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+ )?\[%(addr)s\](?: \(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))$ cmnfailre = ^ruleset=check_rcpt, arg1=(?P<email><\S+@\S+>), relay=(\S+ )?\[%(addr)s\](?: \(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+\]|Fix reverse DNS for \S+)|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=%(addr)s, relay=((?P=dom) )?\[(\d+\.){3}\d+\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$ ^ruleset=check_relay(?:, arg\d+=\S*)*, relay=(\S+ )?\[%(addr)s\](?: \(may be forged\))?, reject=421 4\.3\.2 (Connection rate limit exceeded\.|Too many open connections\.)$
^rejecting commands from (\S* )?\[%(addr)s\] due to pre-greeting traffic after \d+ seconds$ ^rejecting commands from (\S* )?\[%(addr)s\] due to pre-greeting traffic after \d+ seconds$
^(?:\S+ )?\[%(addr)s\]: (?:(?i)expn|vrfy) \S+ \[rejected\]$ ^(?:\S+ )?\[%(addr)s\]: (?:(?i)expn|vrfy) \S+ \[rejected\]$
^<[^@]+@[^>]+>\.\.\. No such user here$ ^<[^@]+@[^>]+>\.\.\. No such user here$

View File

@ -68,15 +68,17 @@ cmnfailed = <cmnfailed-<publickey>>
mdre-normal = mdre-normal =
# used to differentiate "connection closed" with and without `[preauth]` (fail/nofail cases in ddos mode) # used to differentiate "connection closed" with and without `[preauth]` (fail/nofail cases in ddos mode)
mdre-normal-other = ^<F-NOFAIL><F-MLFFORGET>(Connection closed|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__suff)s|\s*)$ mdre-normal-other = ^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__suff)s|\s*)$
mdre-ddos = ^Did not receive identification string from <HOST> mdre-ddos = ^Did not receive identification string from <HOST>
^kex_exchange_identification: (?:[Cc]lient sent invalid protocol identifier|[Cc]onnection closed by remote host) ^kex_exchange_identification: (?:read: )?(?:[Cc]lient sent invalid protocol identifier|[Cc]onnection (?:closed by remote host|reset by peer))
^Bad protocol version identification '.*' from <HOST> ^Bad protocol version identification '.*' from <HOST>
^<F-NOFAIL>SSH: Server;Ltype:</F-NOFAIL> (?:Authname|Version|Kex);Remote: <HOST>-\d+;[A-Z]\w+: ^<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 ^Read from socket failed: Connection <F-MLFFORGET>reset</F-MLFFORGET> by peer
# same as mdre-normal-other, but as failure (without <F-NOFAIL>) and [preauth] only: ^banner exchange: Connection from <HOST><__on_port_opt>: invalid format
# same as mdre-normal-other, but as failure (without <F-NOFAIL> with [preauth] and with <F-NOFAIL> on no preauth phase as helper to identify address):
mdre-ddos-other = ^<F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET> (?:by|from)%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$ mdre-ddos-other = ^<F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET> (?:by|from)%(__authng_user)s <HOST>%(__on_port_opt)s\s+\[preauth\]\s*$
^<F-NOFAIL><F-MLFFORGET>(Connection (?:closed|reset)|Disconnected)</F-MLFFORGET></F-NOFAIL> (?:by|from)%(__authng_user)s <HOST>(?:%(__on_port_opt)s|\s*)$
mdre-extra = ^Received <F-MLFFORGET>disconnect</F-MLFFORGET> from <HOST>%(__on_port_opt)s:\s*14: No(?: supported)? authentication methods available 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 with <HOST>%(__on_port_opt)s: no matching <__alg_match> found.

View File

@ -5,17 +5,23 @@ before = apache-common.conf
[Definition] [Definition]
# pattern: [Wed Apr 27 23:12:07.736196 2016] [:error] [pid 2460] [client 10.1.1.1:47296] WAR [Login denied for user "test"], referer: https://zoneminderurl/index.php # patterns: [Mon Mar 28 16:50:49.522240 2016] [:error] [pid 1795] [client 10.1.1.1:50700] WAR [Login denied for user "username1"], referer: https://zoneminder/
# # [Sun Mar 28 16:53:00.472693 2021] [php7:notice] [pid 11328] [client 10.1.1.1:39568] ERR [Could not retrieve user test details], referer: https://zm/
# [Sun Mar 28 16:59:14.150625 2021] [php7:notice] [pid 11336] [client 10.1.1.1:39654] ERR [Login denied for user "john"], referer: https://zm/
# #
# Option: failregex # Option: failregex
# Notes.: regex to match the password failure messages in the logfile. # Notes.: regex to match the login failure and non-existent user error messages in the logfile.
failregex = ^%(_apache_error_client)s WAR \[Login denied for user "[^"]*"\] prefregex = ^%(_apache_error_client)s (?:ERR|WAR) <F-CONTENT>\[(?:Login denied|Could not retrieve).*</F-CONTENT>$
failregex = ^\[Login denied for user "<F-USER>[^"]*</F-USER>"\]
^\[Could not retrieve user <F-USER>\S*</F-USER>
ignoreregex = ignoreregex =
# Notes: # Notes:
# Tested on Zoneminder 1.29.0 # Tested on Zoneminder 1.29 and 1.35.21
#
# Zoneminder versions > 1.3x use "ERR" and < 1.3x use "WAR" level logs, so i've kept both for compatibility reasons
# #
# Author: John Marzella # Author: John Marzella

View File

@ -67,7 +67,7 @@ before = paths-debian.conf
# more aggressive example of formula has the same values only for factor "2.0 / 2.885385" : # 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.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 # "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
# previously ban count and given "bantime.factor" (for multipliers default is 1); # 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, # 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 # always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
@ -77,7 +77,7 @@ before = paths-debian.conf
#bantime.multipliers = 1 5 30 60 300 720 1440 2880 #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 # "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 # cross over all jails, if false (default), only current jail of the ban IP will be searched
#bantime.overalljails = false #bantime.overalljails = false
# -------------------- # --------------------
@ -227,6 +227,15 @@ action_mwl = %(action_)s
action_xarf = %(action_)s action_xarf = %(action_)s
xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"] xarf-login-attack[service=%(__name__)s, sender="%(sender)s", logpath="%(logpath)s", port="%(port)s"]
# ban & send a notification to one or more of the 50+ services supported by Apprise.
# See https://github.com/caronc/apprise/wiki for details on what is supported.
#
# You may optionally over-ride the default configuration line (containing the Apprise URLs)
# by using 'apprise[config="/alternate/path/to/apprise.cfg"]' otherwise
# /etc/fail2ban/apprise.conf is sourced for your supported notification configuration.
# action = %(action_)s
# apprise
# ban IP on CloudFlare & send an e-mail with whois report and relevant log lines # ban IP on CloudFlare & send an e-mail with whois report and relevant log lines
# to the destemail. # to the destemail.
action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"] action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
@ -242,20 +251,6 @@ action_cf_mwl = cloudflare[cfuser="%(cfemail)s", cftoken="%(cfapikey)s"]
# #
action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"] action_blocklist_de = blocklist_de[email="%(sender)s", service="%(__name__)s", apikey="%(blocklist_de_apikey)s", agent="%(fail2ban_agent)s"]
# Report ban via badips.com, and use as blacklist
#
# See BadIPsAction docstring in config/action.d/badips.py for
# documentation for this action.
#
# NOTE: This action relies on banaction being present on start and therefore
# should be last action defined for a jail.
#
action_badips = badips.py[category="%(__name__)s", banaction="%(banaction)s", agent="%(fail2ban_agent)s"]
#
# Report ban via badips.com (uses action.d/badips.conf for reporting only)
#
action_badips_report = badips[category="%(__name__)s", agent="%(fail2ban_agent)s"]
# Report ban via abuseipdb.com. # Report ban via abuseipdb.com.
# #
# See action.d/abuseipdb.conf for usage example and details. # See action.d/abuseipdb.conf for usage example and details.
@ -351,7 +346,7 @@ maxretry = 2
port = http,https port = http,https
logpath = %(apache_access_log)s logpath = %(apache_access_log)s
maxretry = 1 maxretry = 1
ignorecommand = %(ignorecommands_dir)s/apache-fakegooglebot <ip> ignorecommand = %(fail2ban_confpath)s/filter.d/ignorecommands/apache-fakegooglebot <ip>
[apache-modsecurity] [apache-modsecurity]
@ -375,8 +370,11 @@ banaction = %(banaction_allports)s
logpath = /opt/openhab/logs/request.log logpath = /opt/openhab/logs/request.log
# To use more aggressive http-auth modes set filter parameter "mode" in jail.local:
# normal (default), aggressive (combines all), auth or fallback
# See "tests/files/logs/nginx-http-auth" or "filter.d/nginx-http-auth.conf" for usage example and details.
[nginx-http-auth] [nginx-http-auth]
# mode = normal
port = http,https port = http,https
logpath = %(nginx_error_log)s logpath = %(nginx_error_log)s
@ -392,8 +390,10 @@ logpath = %(nginx_error_log)s
port = http,https port = http,https
logpath = %(nginx_error_log)s logpath = %(nginx_error_log)s
maxretry = 2
[nginx-bad-request]
port = http,https
logpath = %(nginx_access_log)s
# Ban attackers that try to use PHP's URL-fopen() functionality # Ban attackers that try to use PHP's URL-fopen() functionality
# through GET/POST variables. - Experimental, with more than a year # through GET/POST variables. - Experimental, with more than a year
@ -797,6 +797,14 @@ logpath = %(mysql_log)s
backend = %(mysql_backend)s backend = %(mysql_backend)s
[mssql-auth]
# Default configuration for Microsoft SQL Server for Linux
# See the 'mssql-conf' manpage how to change logpath or port
logpath = /var/opt/mssql/log/errorlog
port = 1433
filter = mssql-auth
# Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf') # Log wrong MongoDB auth (for details see filter 'filter.d/mongodb-auth.conf')
[mongodb-auth] [mongodb-auth]
# change port when running with "--shardsvr" or "--configsvr" runtime operation # change port when running with "--shardsvr" or "--configsvr" runtime operation
@ -962,3 +970,11 @@ logpath = %(apache_error_log)s
# see `filter.d/traefik-auth.conf` for details and service example. # see `filter.d/traefik-auth.conf` for details and service example.
port = http,https port = http,https
logpath = /var/log/traefik/access.log logpath = /var/log/traefik/access.log
[scanlogd]
logpath = %(syslog_local0)s
banaction = %(banaction_allports)s
[monitorix]
port = 8080
logpath = /var/log/monitorix-httpd

View File

@ -91,6 +91,3 @@ mysql_log = %(syslog_daemon)s
mysql_backend = %(default_backend)s mysql_backend = %(default_backend)s
roundcube_errors_log = /var/log/roundcube/errors roundcube_errors_log = /var/log/roundcube/errors
# Directory with ignorecommand scripts
ignorecommands_dir = /etc/fail2ban/filter.d/ignorecommands

View File

@ -26,3 +26,5 @@ exim_main_log = /var/log/exim4/mainlog
# was in debian squeezy but not in wheezy # was in debian squeezy but not in wheezy
# /etc/proftpd/proftpd.conf (SystemLog) # /etc/proftpd/proftpd.conf (SystemLog)
proftpd_log = /var/log/proftpd/proftpd.log proftpd_log = /var/log/proftpd/proftpd.log
roundcube_errors_log = /var/log/roundcube/errors.log

View File

@ -175,9 +175,13 @@ class Fail2banClient(Fail2banCmdLine, Thread):
return [["server-stream", stream], ['server-status']] return [["server-stream", stream], ['server-status']]
def _set_server(self, s):
self._server = s
## ##
def __startServer(self, background=True): def __startServer(self, background=True):
from .fail2banserver import Fail2banServer from .fail2banserver import Fail2banServer
# read configuration here (in client only, in server we do that in the config-thread):
stream = self.__prepareStartServer() stream = self.__prepareStartServer()
self._alive = True self._alive = True
if not stream: if not stream:
@ -192,16 +196,19 @@ class Fail2banClient(Fail2banCmdLine, Thread):
return False return False
else: else:
# In foreground mode we should make server/client communication in different threads: # In foreground mode we should make server/client communication in different threads:
th = Thread(target=Fail2banClient.__processStartStreamAfterWait, args=(self, stream, False)) phase = dict()
th.daemon = True self.configureServer(phase=phase, stream=stream)
th.start()
# Mark current (main) thread as daemon: # Mark current (main) thread as daemon:
self.setDaemon(True) self.daemon = True
# Start server direct here in main thread (not fork): # Start server direct here in main thread (not fork):
self._server = Fail2banServer.startServerDirect(self._conf, False) self._server = Fail2banServer.startServerDirect(self._conf, False, self._set_server)
if not phase.get('done', False):
if self._server: # pragma: no cover
self._server.quit()
self._server = None
exit(255)
except ExitException: # pragma: no cover except ExitException: # pragma: no cover
pass raise
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
output("") output("")
logSys.error("Exception while starting server " + ("background" if background else "foreground")) logSys.error("Exception while starting server " + ("background" if background else "foreground"))
@ -214,23 +221,39 @@ class Fail2banClient(Fail2banCmdLine, Thread):
return True return True
## ##
def configureServer(self, nonsync=True, phase=None): def configureServer(self, nonsync=True, phase=None, stream=None):
# if asynchronous start this operation in the new thread: # if asynchronous start this operation in the new thread:
if nonsync: if nonsync:
th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase)) if phase is not None:
# event for server ready flag:
def _server_ready():
phase['start-ready'] = True
logSys.log(5, ' server phase %s', phase)
# notify waiting thread if server really ready
self._conf['onstart'] = _server_ready
th = Thread(target=Fail2banClient.configureServer, args=(self, False, phase, stream))
th.daemon = True th.daemon = True
return th.start() th.start()
# if we need to read configuration stream:
if stream is None and phase is not None:
# wait, do not continue if configuration is not 100% valid:
Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"], 0.001)
logSys.log(5, ' server phase %s', phase)
if not phase.get('start', False):
raise ServerExecutionException('Async configuration of server failed')
return True
# prepare: read config, check configuration is valid, etc.: # prepare: read config, check configuration is valid, etc.:
if phase is not None: if phase is not None:
phase['start'] = True phase['start'] = True
logSys.log(5, ' client phase %s', phase) logSys.log(5, ' client phase %s', phase)
if stream is None:
stream = self.__prepareStartServer() stream = self.__prepareStartServer()
if phase is not None: if phase is not None:
phase['ready'] = phase['start'] = (True if stream else False) phase['ready'] = phase['start'] = (True if stream else False)
logSys.log(5, ' client phase %s', phase) logSys.log(5, ' client phase %s', phase)
if not stream: if not stream:
return False return False
# wait a litle bit for phase "start-ready" before enter active waiting: # wait a little bit for phase "start-ready" before enter active waiting:
if phase is not None: if phase is not None:
Utils.wait_for(lambda: phase.get('start-ready', None) is not None, 0.5, 0.001) Utils.wait_for(lambda: phase.get('start-ready', None) is not None, 0.5, 0.001)
phase['configure'] = (True if stream else False) phase['configure'] = (True if stream else False)
@ -321,13 +344,14 @@ class Fail2banClient(Fail2banCmdLine, Thread):
def __processStartStreamAfterWait(self, *args): def __processStartStreamAfterWait(self, *args):
ret = False
try: try:
# Wait for the server to start # Wait for the server to start
if not self.__waitOnServer(): # pragma: no cover if not self.__waitOnServer(): # pragma: no cover
logSys.error("Could not find server, waiting failed") logSys.error("Could not find server, waiting failed")
return False return False
# Configure the server # Configure the server
self.__processCmd(*args) ret = self.__processCmd(*args)
except ServerExecutionException as e: # pragma: no cover except ServerExecutionException as e: # pragma: no cover
if self._conf["verbose"] > 1: if self._conf["verbose"] > 1:
logSys.exception(e) logSys.exception(e)
@ -336,10 +360,11 @@ class Fail2banClient(Fail2banCmdLine, Thread):
"remove " + self._conf["socket"] + ". If " "remove " + self._conf["socket"] + ". If "
"you used fail2ban-client to start the " "you used fail2ban-client to start the "
"server, adding the -x option will do it") "server, adding the -x option will do it")
if self._server:
if not ret and self._server: # stop on error (foreground, config read in another thread):
self._server.quit() self._server.quit()
return False self._server = None
return True return ret
def __waitOnServer(self, alive=True, maxtime=None): def __waitOnServer(self, alive=True, maxtime=None):
if maxtime is None: if maxtime is None:

View File

@ -192,7 +192,7 @@ class Fail2banCmdLine():
cmdOpts = 'hc:s:p:xfbdtviqV' cmdOpts = 'hc:s:p:xfbdtviqV'
cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async', cmdLongOpts = ['loglevel=', 'logtarget=', 'syslogsocket=', 'test', 'async',
'conf=', 'pidfile=', 'pname=', 'socket=', 'conf=', 'pidfile=', 'pname=', 'socket=',
'timeout=', 'str2sec=', 'help', 'version', 'dp', '--dump-pretty'] 'timeout=', 'str2sec=', 'help', 'version', 'dp', 'dump-pretty']
optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts) optList, self._args = getopt.getopt(self._argv[1:], cmdOpts, cmdLongOpts)
except getopt.GetoptError: except getopt.GetoptError:
self.dispUsage() self.dispUsage()

View File

@ -53,6 +53,7 @@ class Fail2banReader(ConfigReader):
opts = [["string", "loglevel", "INFO" ], opts = [["string", "loglevel", "INFO" ],
["string", "logtarget", "STDERR"], ["string", "logtarget", "STDERR"],
["string", "syslogsocket", "auto"], ["string", "syslogsocket", "auto"],
["string", "allowipv6", "auto"],
["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"], ["string", "dbfile", "/var/lib/fail2ban/fail2ban.sqlite3"],
["int", "dbmaxmatches", None], ["int", "dbmaxmatches", None],
["string", "dbpurgeage", "1d"]] ["string", "dbpurgeage", "1d"]]
@ -74,6 +75,7 @@ class Fail2banReader(ConfigReader):
# Also dbfile should be set before all other database options. # Also dbfile should be set before all other database options.
# So adding order indices into items, to be stripped after sorting, upon return # So adding order indices into items, to be stripped after sorting, upon return
order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13, order = {"thread":0, "syslogsocket":11, "loglevel":12, "logtarget":13,
"allowipv6": 14,
"dbfile":50, "dbmaxmatches":51, "dbpurgeage":51} "dbfile":50, "dbmaxmatches":51, "dbpurgeage":51}
stream = list() stream = list()
for opt in self.__opts: for opt in self.__opts:

View File

@ -35,11 +35,11 @@ __license__ = "GPL"
import getopt import getopt
import logging import logging
import re
import os import os
import shlex import shlex
import sys import sys
import time import time
import time
import urllib import urllib
from optparse import OptionParser, Option from optparse import OptionParser, Option
@ -52,7 +52,7 @@ except ImportError:
from ..version import version, normVersion from ..version import version, normVersion
from .filterreader import FilterReader from .filterreader import FilterReader
from ..server.filter import Filter, FileContainer from ..server.filter import Filter, FileContainer, MyTime
from ..server.failregex import Regex, RegexException from ..server.failregex import Regex, RegexException
from ..helpers import str2LogLevel, getVerbosityFormat, FormatterWithTraceBack, getLogger, \ from ..helpers import str2LogLevel, getVerbosityFormat, FormatterWithTraceBack, getLogger, \
@ -269,15 +269,19 @@ class Fail2banRegex(object):
self.setJournalMatch(shlex.split(opts.journalmatch)) self.setJournalMatch(shlex.split(opts.journalmatch))
if opts.timezone: if opts.timezone:
self._filter.setLogTimeZone(opts.timezone) self._filter.setLogTimeZone(opts.timezone)
self._filter.checkFindTime = False
if True: # not opts.out:
MyTime.setAlternateNow(0); # accept every date (years from 19xx up to end of current century, '%ExY' and 'Exy' patterns)
from ..server.strptime import _updateTimeRE
_updateTimeRE()
if opts.datepattern: if opts.datepattern:
self.setDatePattern(opts.datepattern) self.setDatePattern(opts.datepattern)
if opts.usedns: if opts.usedns:
self._filter.setUseDns(opts.usedns) self._filter.setUseDns(opts.usedns)
self._filter.returnRawHost = opts.raw self._filter.returnRawHost = opts.raw
self._filter.checkFindTime = False
self._filter.checkAllRegex = opts.checkAllRegex and not opts.out self._filter.checkAllRegex = opts.checkAllRegex and not opts.out
# ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retreved) # ignore pending (without ID/IP), added to matches if it hits later (if ID/IP can be retreved)
self._filter.ignorePending = opts.out self._filter.ignorePending = bool(opts.out)
# callback to increment ignored RE's by index (during process): # callback to increment ignored RE's by index (during process):
self._filter.onIgnoreRegex = self._onIgnoreRegex self._filter.onIgnoreRegex = self._onIgnoreRegex
self._backend = 'auto' self._backend = 'auto'
@ -285,9 +289,6 @@ class Fail2banRegex(object):
def output(self, line): def output(self, line):
if not self._opts.out: output(line) if not self._opts.out: output(line)
def decode_line(self, line):
return FileContainer.decode_line('<LOG>', self._encoding, line)
def encode_line(self, line): def encode_line(self, line):
return line.encode(self._encoding, 'ignore') return line.encode(self._encoding, 'ignore')
@ -326,11 +327,13 @@ class Fail2banRegex(object):
regex = regextype + 'regex' regex = regextype + 'regex'
# try to check - we've case filter?[options...]?: # try to check - we've case filter?[options...]?:
basedir = self._opts.config basedir = self._opts.config
fltName = value
fltFile = None fltFile = None
fltOpt = {} fltOpt = {}
if regextype == 'fail': if regextype == 'fail':
if re.search(r'(?ms)^/{0,3}[\w/_\-.]+(?:\[.*\])?$', value):
try:
fltName, fltOpt = extractOptions(value) fltName, fltOpt = extractOptions(value)
if fltName is not None:
if "." in fltName[~5:]: if "." in fltName[~5:]:
tryNames = (fltName,) tryNames = (fltName,)
else: else:
@ -346,6 +349,11 @@ class Fail2banRegex(object):
if os.path.isfile(fltFile): if os.path.isfile(fltFile):
break break
fltFile = None fltFile = None
except Exception as e:
output("ERROR: Wrong filter name or options: %s" % (str(e),))
output(" while parsing: %s" % (value,))
if self._verbose: raise(e)
return False
# if it is filter file: # if it is filter file:
if fltFile is not None: if fltFile is not None:
if (basedir == self._opts.config if (basedir == self._opts.config
@ -512,10 +520,14 @@ class Fail2banRegex(object):
def _prepaireOutput(self): def _prepaireOutput(self):
"""Prepares output- and fetch-function corresponding given '--out' option (format)""" """Prepares output- and fetch-function corresponding given '--out' option (format)"""
ofmt = self._opts.out ofmt = self._opts.out
if ofmt in ('id', 'ip'): if ofmt in ('id', 'fid'):
def _out(ret): def _out(ret):
for r in ret: for r in ret:
output(r[1]) output(r[1])
elif ofmt == 'ip':
def _out(ret):
for r in ret:
output(r[3].get('ip', r[1]))
elif ofmt == 'msg': elif ofmt == 'msg':
def _out(ret): def _out(ret):
for r in ret: for r in ret:
@ -712,10 +724,6 @@ class Fail2banRegex(object):
return True return True
def file_lines_gen(self, hdlr):
for line in hdlr:
yield self.decode_line(line)
def start(self, args): def start(self, args):
cmd_log, cmd_regex = args[:2] cmd_log, cmd_regex = args[:2]
@ -734,10 +742,10 @@ class Fail2banRegex(object):
if os.path.isfile(cmd_log): if os.path.isfile(cmd_log):
try: try:
hdlr = open(cmd_log, 'rb') test_lines = FileContainer(cmd_log, self._encoding, doOpen=True)
self.output( "Use log file : %s" % cmd_log ) self.output( "Use log file : %s" % cmd_log )
self.output( "Use encoding : %s" % self._encoding ) self.output( "Use encoding : %s" % self._encoding )
test_lines = self.file_lines_gen(hdlr)
except IOError as e: # pragma: no cover except IOError as e: # pragma: no cover
output( e ) output( e )
return False return False

View File

@ -44,7 +44,7 @@ class Fail2banServer(Fail2banCmdLine):
# Start the Fail2ban server in background/foreground (daemon mode or not). # Start the Fail2ban server in background/foreground (daemon mode or not).
@staticmethod @staticmethod
def startServerDirect(conf, daemon=True): def startServerDirect(conf, daemon=True, setServer=None):
logSys.debug(" direct starting of server in %s, deamon: %s", os.getpid(), daemon) logSys.debug(" direct starting of server in %s, deamon: %s", os.getpid(), daemon)
from ..server.server import Server from ..server.server import Server
server = None server = None
@ -52,6 +52,10 @@ class Fail2banServer(Fail2banCmdLine):
# Start it in foreground (current thread, not new process), # Start it in foreground (current thread, not new process),
# server object will internally fork self if daemon is True # server object will internally fork self if daemon is True
server = Server(daemon) server = Server(daemon)
# notify caller - set server handle:
if setServer:
setServer(server)
# run:
server.start(conf["socket"], server.start(conf["socket"],
conf["pidfile"], conf["force"], conf["pidfile"], conf["force"],
conf=conf) conf=conf)
@ -63,6 +67,10 @@ class Fail2banServer(Fail2banCmdLine):
if conf["verbose"] > 1: if conf["verbose"] > 1:
logSys.exception(e2) logSys.exception(e2)
raise raise
finally:
# notify waiting thread server ready resp. done (background execution, error case, etc):
if conf.get('onstart'):
conf['onstart']()
return server return server
@ -179,27 +187,15 @@ class Fail2banServer(Fail2banCmdLine):
# Start new thread with client to read configuration and # Start new thread with client to read configuration and
# transfer it to the server: # transfer it to the server:
cli = self._Fail2banClient() cli = self._Fail2banClient()
cli._conf = self._conf
phase = dict() phase = dict()
logSys.debug('Configure via async client thread') logSys.debug('Configure via async client thread')
cli.configureServer(phase=phase) cli.configureServer(phase=phase)
# wait, do not continue if configuration is not 100% valid:
Utils.wait_for(lambda: phase.get('ready', None) is not None, self._conf["timeout"], 0.001)
logSys.log(5, ' server phase %s', phase)
if not phase.get('start', False):
raise ServerExecutionException('Async configuration of server failed')
# event for server ready flag:
def _server_ready():
phase['start-ready'] = True
logSys.log(5, ' server phase %s', phase)
# notify waiting thread if server really ready
self._conf['onstart'] = _server_ready
# Start server, daemonize it, etc. # Start server, daemonize it, etc.
pid = os.getpid() pid = os.getpid()
server = Fail2banServer.startServerDirect(self._conf, background) server = Fail2banServer.startServerDirect(self._conf, background,
# notify waiting thread server ready resp. done (background execution, error case, etc): cli._set_server if cli else None)
if not nonsync:
_server_ready()
# If forked - just exit other processes # If forked - just exit other processes
if pid != os.getpid(): # pragma: no cover if pid != os.getpid(): # pragma: no cover
os._exit(0) os._exit(0)

View File

@ -72,8 +72,9 @@ class FilterReader(DefinitionInitConfigReader):
def _fillStream(stream, opts, jailName): def _fillStream(stream, opts, jailName):
prio0idx = 0 prio0idx = 0
for opt, value in opts.iteritems(): for opt, value in opts.iteritems():
if opt in ("failregex", "ignoreregex"): # Do not send a command if the value is not set (empty).
if value is None: continue if value is None: continue
if opt in ("failregex", "ignoreregex"):
multi = [] multi = []
for regex in value.split('\n'): for regex in value.split('\n'):
# Do not send a command if the rule is empty. # Do not send a command if the rule is empty.
@ -91,8 +92,6 @@ class FilterReader(DefinitionInitConfigReader):
elif opt in ('datepattern'): elif opt in ('datepattern'):
stream.append(["set", jailName, opt, value]) stream.append(["set", jailName, opt, value])
elif opt == 'journalmatch': elif opt == 'journalmatch':
# Do not send a command if the match is empty.
if value is None: continue
for match in value.split("\n"): for match in value.split("\n"):
if match == '': continue if match == '': continue
stream.append( stream.append(

View File

@ -121,9 +121,12 @@ class JailReader(ConfigReader):
def getOptions(self): def getOptions(self):
basedir = self.getBaseDir()
# Before interpolation (substitution) add static options always available as default: # Before interpolation (substitution) add static options always available as default:
self.merge_defaults({ self.merge_defaults({
"fail2ban_version": version "fail2ban_version": version,
"fail2ban_confpath": basedir
}) })
try: try:
@ -140,12 +143,13 @@ class JailReader(ConfigReader):
# Read filter # Read filter
flt = self.__opts["filter"] flt = self.__opts["filter"]
if flt: if flt:
try:
filterName, filterOpt = extractOptions(flt) filterName, filterOpt = extractOptions(flt)
if not filterName: except ValueError as e:
raise JailDefError("Invalid filter definition %r" % flt) raise JailDefError("Invalid filter definition %r: %s" % (flt, e))
self.__filter = FilterReader( self.__filter = FilterReader(
filterName, self.__name, filterOpt, filterName, self.__name, filterOpt,
share_config=self.share_config, basedir=self.getBaseDir()) share_config=self.share_config, basedir=basedir)
ret = self.__filter.read() ret = self.__filter.read()
if not ret: if not ret:
raise JailDefError("Unable to read the filter %r" % filterName) raise JailDefError("Unable to read the filter %r" % filterName)
@ -174,10 +178,10 @@ class JailReader(ConfigReader):
if not act: # skip empty actions if not act: # skip empty actions
continue continue
# join with previous line if needed (consider possible new-line): # join with previous line if needed (consider possible new-line):
try:
actName, actOpt = extractOptions(act) actName, actOpt = extractOptions(act)
prevln = '' except ValueError as e:
if not actName: raise JailDefError("Invalid action definition %r: %s" % (act, e))
raise JailDefError("Invalid action definition %r" % act)
if actName.endswith(".py"): if actName.endswith(".py"):
self.__actions.append([ self.__actions.append([
"set", "set",
@ -185,13 +189,13 @@ class JailReader(ConfigReader):
"addaction", "addaction",
actOpt.pop("actname", os.path.splitext(actName)[0]), actOpt.pop("actname", os.path.splitext(actName)[0]),
os.path.join( os.path.join(
self.getBaseDir(), "action.d", actName), basedir, "action.d", actName),
json.dumps(actOpt), json.dumps(actOpt),
]) ])
else: else:
action = ActionReader( action = ActionReader(
actName, self.__name, actOpt, actName, self.__name, actOpt,
share_config=self.share_config, basedir=self.getBaseDir()) share_config=self.share_config, basedir=basedir)
ret = action.read() ret = action.read()
if ret: if ret:
action.getOptions(self.__opts) action.getOptions(self.__opts)

View File

@ -371,7 +371,7 @@ OPTION_CRE = re.compile(r"^([^\[]+)(?:\[(.*)\])?\s*$", re.DOTALL)
# since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax # since v0.10 separator extended with `]\s*[` for support of multiple option groups, syntax
# `action = act[p1=...][p2=...]` # `action = act[p1=...][p2=...]`
OPTION_EXTRACT_CRE = re.compile( OPTION_EXTRACT_CRE = re.compile(
r'([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$)', re.DOTALL) r'\s*([\w\-_\.]+)=(?:"([^"]*)"|\'([^\']*)\'|([^,\]]*))(?:,|\]\s*\[|$|(?P<wrngA>.+))|,?\s*$|(?P<wrngB>.+)', re.DOTALL)
# split by new-line considering possible new-lines within options [...]: # split by new-line considering possible new-lines within options [...]:
OPTION_SPLIT_CRE = re.compile( OPTION_SPLIT_CRE = re.compile(
r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL) r'(?:[^\[\s]+(?:\s*\[\s*(?:[\w\-_\.]+=(?:"[^"]*"|\'[^\']*\'|[^,\]]*)\s*(?:,|\]\s*\[)?\s*)*\])?\s*|\S+)(?=\n\s*|\s+|$)', re.DOTALL)
@ -379,13 +379,19 @@ OPTION_SPLIT_CRE = re.compile(
def extractOptions(option): def extractOptions(option):
match = OPTION_CRE.match(option) match = OPTION_CRE.match(option)
if not match: if not match:
# TODO proper error handling raise ValueError("unexpected option syntax")
return None, None
option_name, optstr = match.groups() option_name, optstr = match.groups()
option_opts = dict() option_opts = dict()
if optstr: if optstr:
for optmatch in OPTION_EXTRACT_CRE.finditer(optstr): for optmatch in OPTION_EXTRACT_CRE.finditer(optstr):
if optmatch.group("wrngA"):
raise ValueError("unexpected syntax at %d after option %r: %s" % (
optmatch.start("wrngA"), optmatch.group(1), optmatch.group("wrngA")[0:25]))
if optmatch.group("wrngB"):
raise ValueError("expected option, wrong syntax at %d: %s" % (
optmatch.start("wrngB"), optmatch.group("wrngB")[0:25]))
opt = optmatch.group(1) opt = optmatch.group(1)
if not opt: continue
value = [ value = [
val for val in optmatch.group(2,3,4) if val is not None][0] val for val in optmatch.group(2,3,4) if val is not None][0]
option_opts[opt.strip()] = value.strip() option_opts[opt.strip()] = value.strip()

View File

@ -66,7 +66,7 @@ protocol = [
["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, " ["set loglevel <LEVEL>", "sets logging level to <LEVEL>. Levels: CRITICAL, ERROR, WARNING, NOTICE, INFO, "
"DEBUG, TRACEDEBUG, HEAVYDEBUG or corresponding numeric value (50-5)"], "DEBUG, TRACEDEBUG, HEAVYDEBUG or corresponding numeric value (50-5)"],
["get loglevel", "gets the logging level"], ["get loglevel", "gets the logging level"],
["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG or a file"], ["set logtarget <TARGET>", "sets logging target to <TARGET>. Can be STDOUT, STDERR, SYSLOG, SYSTEMD-JOURNAL or a file"],
["get logtarget", "gets logging target"], ["get logtarget", "gets logging target"],
["set syslogsocket auto|<SOCKET>", "sets the syslog socket path to auto or <SOCKET>. Only used if logtarget is SYSLOG"], ["set syslogsocket auto|<SOCKET>", "sets the syslog socket path to auto or <SOCKET>. Only used if logtarget is SYSLOG"],
["get syslogsocket", "gets syslog socket path"], ["get syslogsocket", "gets syslog socket path"],
@ -134,7 +134,7 @@ protocol = [
["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"], ["get <JAIL> ignoreregex", "gets the list of regular expressions which matches patterns to ignore for <JAIL>"],
["get <JAIL> findtime", "gets the time for which the filter will look back for failures for <JAIL>"], ["get <JAIL> findtime", "gets the time for which the filter will look back for failures for <JAIL>"],
["get <JAIL> bantime", "gets the time a host is banned for <JAIL>"], ["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> datepattern", "gets the pattern used to match date/times for <JAIL>"],
["get <JAIL> usedns", "gets the usedns setting 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> 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> maxretry", "gets the number of failures allowed for <JAIL>"],

View File

@ -30,6 +30,9 @@ import tempfile
import threading import threading
import time import time
from abc import ABCMeta from abc import ABCMeta
try:
from collections.abc import MutableMapping
except ImportError:
from collections import MutableMapping from collections import MutableMapping
from .failregex import mapTag2Opt from .failregex import mapTag2Opt
@ -407,7 +410,7 @@ class CommandAction(ActionBase):
cmd = self.replaceTag(tag, self._properties, cmd = self.replaceTag(tag, self._properties,
conditional=('family='+family if family else ''), conditional=('family='+family if family else ''),
cache=self.__substCache) cache=self.__substCache)
if '<' not in cmd or not family: return cmd if not family or '<' not in cmd: return cmd
# replace family as dynamic tags, important - don't cache, no recursion and auto-escape here: # replace family as dynamic tags, important - don't cache, no recursion and auto-escape here:
cmd = self.replaceDynamicTags(cmd, {'family':family}) cmd = self.replaceDynamicTags(cmd, {'family':family})
return cmd return cmd
@ -974,8 +977,11 @@ class CommandAction(ActionBase):
except (KeyError, TypeError): except (KeyError, TypeError):
family = '' family = ''
# invariant check: repcnt = 0
if self.actioncheck: while True:
# got some error, do invariant check:
if repcnt and self.actioncheck:
# don't repair/restore if unban (no matter): # don't repair/restore if unban (no matter):
def _beforeRepair(): def _beforeRepair():
if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'): if cmd == '<actionunban>' and not self._properties.get('actionrepair_on_unban'):
@ -998,7 +1004,11 @@ class CommandAction(ActionBase):
else: else:
realCmd = cmd realCmd = cmd
return self.executeCmd(realCmd, self.timeout) # try execute command:
ret = self.executeCmd(realCmd, self.timeout)
repcnt += 1
if ret or repcnt > 1:
return ret
@staticmethod @staticmethod
def executeCmd(realCmd, timeout=60, **kwargs): def executeCmd(realCmd, timeout=60, **kwargs):

View File

@ -28,11 +28,11 @@ import logging
import os import os
import sys import sys
import time import time
from collections import Mapping
try: try:
from collections import OrderedDict from collections.abc import Mapping
except ImportError: except ImportError:
OrderedDict = dict from collections import Mapping
from collections import OrderedDict
from .banmanager import BanManager, BanTicket from .banmanager import BanManager, BanTicket
from .ipdns import IPAddr from .ipdns import IPAddr
@ -81,7 +81,7 @@ class Actions(JailThread, Mapping):
self._jail = jail self._jail = jail
self._actions = OrderedDict() self._actions = OrderedDict()
## The ban manager. ## The ban manager.
self.__banManager = BanManager() self.banManager = BanManager()
self.banEpoch = 0 self.banEpoch = 0
self.__lastConsistencyCheckTM = 0 self.__lastConsistencyCheckTM = 0
## Precedence of ban (over unban), so max number of tickets banned (to call an unban check): ## Precedence of ban (over unban), so max number of tickets banned (to call an unban check):
@ -200,7 +200,7 @@ class Actions(JailThread, Mapping):
def setBanTime(self, value): def setBanTime(self, value):
value = MyTime.str2seconds(value) value = MyTime.str2seconds(value)
self.__banManager.setBanTime(value) self.banManager.setBanTime(value)
logSys.info(" banTime: %s" % value) logSys.info(" banTime: %s" % value)
## ##
@ -209,10 +209,10 @@ class Actions(JailThread, Mapping):
# @return the time # @return the time
def getBanTime(self): def getBanTime(self):
return self.__banManager.getBanTime() return self.banManager.getBanTime()
def getBanned(self, ids): def getBanned(self, ids):
lst = self.__banManager.getBanList() lst = self.banManager.getBanList()
if not ids: if not ids:
return lst return lst
if len(ids) == 1: if len(ids) == 1:
@ -227,7 +227,7 @@ class Actions(JailThread, Mapping):
list list
The list of banned IP addresses. The list of banned IP addresses.
""" """
return self.__banManager.getBanList(ordered=True, withTime=withTime) return self.banManager.getBanList(ordered=True, withTime=withTime)
def addBannedIP(self, ip): def addBannedIP(self, ip):
"""Ban an IP or list of IPs.""" """Ban an IP or list of IPs."""
@ -279,7 +279,7 @@ class Actions(JailThread, Mapping):
if db and self._jail.database is not None: if db and self._jail.database is not None:
self._jail.database.delBan(self._jail, ip) self._jail.database.delBan(self._jail, ip)
# Find the ticket with the IP. # Find the ticket with the IP.
ticket = self.__banManager.getTicketByID(ip) ticket = self.banManager.getTicketByID(ip)
if ticket is not None: if ticket is not None:
# Unban the IP. # Unban the IP.
self.__unBan(ticket) self.__unBan(ticket)
@ -288,7 +288,7 @@ class Actions(JailThread, Mapping):
if not isinstance(ip, IPAddr): if not isinstance(ip, IPAddr):
ipa = IPAddr(ip) ipa = IPAddr(ip)
if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname): if not ipa.isSingle: # subnet (mask/cidr) or raw (may be dns/hostname):
ips = filter(ipa.contains, self.__banManager.getBanList()) ips = filter(ipa.contains, self.banManager.getBanList())
if ips: if ips:
return self.removeBannedIP(ips, db, ifexists) return self.removeBannedIP(ips, db, ifexists)
# not found: # not found:
@ -305,9 +305,7 @@ class Actions(JailThread, Mapping):
""" """
if actions is None: if actions is None:
actions = self._actions actions = self._actions
revactions = actions.items() for name, action in reversed(actions.items()):
revactions.reverse()
for name, action in revactions:
try: try:
action.stop() action.stop()
except Exception as e: except Exception as e:
@ -347,7 +345,7 @@ class Actions(JailThread, Mapping):
continue continue
# wait for ban (stop if gets inactive, pending ban or unban): # wait for ban (stop if gets inactive, pending ban or unban):
bancnt = 0 bancnt = 0
wt = min(self.sleeptime, self.__banManager._nextUnbanTime - MyTime.time()) wt = min(self.sleeptime, self.banManager._nextUnbanTime - MyTime.time())
logSys.log(5, "Actions: wait for pending tickets %s (default %s)", wt, self.sleeptime) logSys.log(5, "Actions: wait for pending tickets %s (default %s)", wt, self.sleeptime)
if Utils.wait_for(lambda: not self.active or self._jail.hasFailTickets, wt): if Utils.wait_for(lambda: not self.active or self._jail.hasFailTickets, wt):
bancnt = self.__checkBan() bancnt = self.__checkBan()
@ -394,7 +392,12 @@ class Actions(JailThread, Mapping):
"ipfailures": lambda self: self._mi4ip(True).getAttempt(), "ipfailures": lambda self: self._mi4ip(True).getAttempt(),
"ipjailfailures": lambda self: self._mi4ip().getAttempt(), "ipjailfailures": lambda self: self._mi4ip().getAttempt(),
# raw ticket info: # raw ticket info:
"raw-ticket": lambda self: repr(self.__ticket) "raw-ticket": lambda self: repr(self.__ticket),
# jail info:
"jail.banned": lambda self: self.__jail.actions.banManager.size(),
"jail.banned_total": lambda self: self.__jail.actions.banManager.getBanTotal(),
"jail.found": lambda self: self.__jail.filter.failManager.size(),
"jail.found_total": lambda self: self.__jail.filter.failManager.getFailTotal()
} }
__slots__ = CallingMap.__slots__ + ('__ticket', '__jail', '__mi4ip') __slots__ = CallingMap.__slots__ + ('__ticket', '__jail', '__mi4ip')
@ -491,11 +494,11 @@ class Actions(JailThread, Mapping):
for ticket in tickets: for ticket in tickets:
bTicket = BanTicket.wrap(ticket) bTicket = BanTicket.wrap(ticket)
btime = ticket.getBanTime(self.__banManager.getBanTime()) btime = ticket.getBanTime(self.banManager.getBanTime())
ip = bTicket.getIP() ip = bTicket.getID()
aInfo = self._getActionInfo(bTicket) aInfo = self._getActionInfo(bTicket)
reason = {} reason = {}
if self.__banManager.addBanTicket(bTicket, reason=reason): if self.banManager.addBanTicket(bTicket, reason=reason):
cnt += 1 cnt += 1
# report ticket to observer, to check time should be increased and hereafter observer writes ban to database (asynchronous) # 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: if Observers.Main is not None and not bTicket.restored:
@ -539,9 +542,10 @@ class Actions(JailThread, Mapping):
if bTicket.banEpoch == self.banEpoch and diftm > 3: if bTicket.banEpoch == self.banEpoch and diftm > 3:
# avoid too often checks: # avoid too often checks:
if not rebanacts and MyTime.time() > self.__lastConsistencyCheckTM + 3: if not rebanacts and MyTime.time() > self.__lastConsistencyCheckTM + 3:
for action in self._actions.itervalues():
action.consistencyCheck()
self.__lastConsistencyCheckTM = MyTime.time() self.__lastConsistencyCheckTM = MyTime.time()
for action in self._actions.itervalues():
if hasattr(action, 'consistencyCheck'):
action.consistencyCheck()
# check epoch in order to reban it: # check epoch in order to reban it:
if bTicket.banEpoch < self.banEpoch: if bTicket.banEpoch < self.banEpoch:
if not rebanacts: rebanacts = dict( if not rebanacts: rebanacts = dict(
@ -554,7 +558,7 @@ class Actions(JailThread, Mapping):
# and increase ticket time if "bantime.increment" set) # and increase ticket time if "bantime.increment" set)
if cnt: if cnt:
logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt, logSys.debug("Banned %s / %s, %s ticket(s) in %r", cnt,
self.__banManager.getBanTotal(), self.__banManager.size(), self._jail.name) self.banManager.getBanTotal(), self.banManager.size(), self._jail.name)
return cnt return cnt
def __reBan(self, ticket, actions=None, log=True): def __reBan(self, ticket, actions=None, log=True):
@ -569,10 +573,10 @@ class Actions(JailThread, Mapping):
Ticket to reban Ticket to reban
""" """
actions = actions or self._actions actions = actions or self._actions
ip = ticket.getIP() ip = ticket.getID()
aInfo = self._getActionInfo(ticket) aInfo = self._getActionInfo(ticket)
if log: if log:
logSys.notice("[%s] Reban %s%s", self._jail.name, aInfo["ip"], (', action %r' % actions.keys()[0] if len(actions) == 1 else '')) logSys.notice("[%s] Reban %s%s", self._jail.name, ip, (', action %r' % actions.keys()[0] if len(actions) == 1 else ''))
for name, action in actions.iteritems(): for name, action in actions.iteritems():
try: try:
logSys.debug("[%s] action %r: reban %s", self._jail.name, name, ip) logSys.debug("[%s] action %r: reban %s", self._jail.name, name, ip)
@ -594,7 +598,7 @@ class Actions(JailThread, Mapping):
def _prolongBan(self, ticket): def _prolongBan(self, ticket):
# prevent to prolong ticket that was removed in-between, # prevent to prolong ticket that was removed in-between,
# if it in ban list - ban time already prolonged (and it stays there): # if it in ban list - ban time already prolonged (and it stays there):
if not self.__banManager._inBanList(ticket): return if not self.banManager._inBanList(ticket): return
# do actions : # do actions :
aInfo = None aInfo = None
for name, action in self._actions.iteritems(): for name, action in self._actions.iteritems():
@ -619,13 +623,13 @@ class Actions(JailThread, Mapping):
Unban IP addresses which are outdated. Unban IP addresses which are outdated.
""" """
lst = self.__banManager.unBanList(MyTime.time(), maxCount) lst = self.banManager.unBanList(MyTime.time(), maxCount)
for ticket in lst: for ticket in lst:
self.__unBan(ticket) self.__unBan(ticket)
cnt = len(lst) cnt = len(lst)
if cnt: if cnt:
logSys.debug("Unbanned %s, %s ticket(s) in %r", logSys.debug("Unbanned %s, %s ticket(s) in %r",
cnt, self.__banManager.size(), self._jail.name) cnt, self.banManager.size(), self._jail.name)
return cnt return cnt
def __flushBan(self, db=False, actions=None, stop=False): def __flushBan(self, db=False, actions=None, stop=False):
@ -639,10 +643,10 @@ class Actions(JailThread, Mapping):
log = True log = True
if actions is None: if actions is None:
logSys.debug(" Flush ban list") logSys.debug(" Flush ban list")
lst = self.__banManager.flushBanList() lst = self.banManager.flushBanList()
else: else:
log = False # don't log "[jail] Unban ..." if removing actions only. log = False # don't log "[jail] Unban ..." if removing actions only.
lst = iter(self.__banManager) lst = iter(self.banManager)
cnt = 0 cnt = 0
# first we'll execute flush for actions supporting this operation: # first we'll execute flush for actions supporting this operation:
unbactions = {} unbactions = {}
@ -660,7 +664,7 @@ class Actions(JailThread, Mapping):
if hasattr(action, 'consistencyCheck'): if hasattr(action, 'consistencyCheck'):
def _beforeRepair(): def _beforeRepair():
if stop and not getattr(action, 'actionrepair_on_unban', None): # don't need repair on stop 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.") logSys.error("Invariant check failed. Flush is impossible.")
return False return False
return True return True
action.consistencyCheck(_beforeRepair) action.consistencyCheck(_beforeRepair)
@ -679,7 +683,7 @@ class Actions(JailThread, Mapping):
self.__unBan(ticket, actions=actions, log=log) self.__unBan(ticket, actions=actions, log=log)
cnt += 1 cnt += 1
logSys.debug(" Unbanned %s, %s ticket(s) in %r", logSys.debug(" Unbanned %s, %s ticket(s) in %r",
cnt, self.__banManager.size(), self._jail.name) cnt, self.banManager.size(), self._jail.name)
return cnt return cnt
def __unBan(self, ticket, actions=None, log=True): def __unBan(self, ticket, actions=None, log=True):
@ -697,10 +701,10 @@ class Actions(JailThread, Mapping):
unbactions = self._actions unbactions = self._actions
else: else:
unbactions = actions unbactions = actions
ip = ticket.getIP() ip = ticket.getID()
aInfo = self._getActionInfo(ticket) aInfo = self._getActionInfo(ticket)
if log: if log:
logSys.notice("[%s] Unban %s", self._jail.name, aInfo["ip"]) logSys.notice("[%s] Unban %s", self._jail.name, ip)
for name, action in unbactions.iteritems(): for name, action in unbactions.iteritems():
try: try:
logSys.debug("[%s] action %r: unban %s", self._jail.name, name, ip) logSys.debug("[%s] action %r: unban %s", self._jail.name, name, ip)
@ -722,18 +726,18 @@ class Actions(JailThread, Mapping):
logSys.warning("Unsupported extended jail status flavor %r. Supported: %s" % (flavor, supported_flavors)) logSys.warning("Unsupported extended jail status flavor %r. Supported: %s" % (flavor, supported_flavors))
# Always print this information (basic) # Always print this information (basic)
if flavor != "short": if flavor != "short":
banned = self.__banManager.getBanList() banned = self.banManager.getBanList()
cnt = len(banned) cnt = len(banned)
else: else:
cnt = self.__banManager.size() cnt = self.banManager.size()
ret = [("Currently banned", cnt), ret = [("Currently banned", cnt),
("Total banned", self.__banManager.getBanTotal())] ("Total banned", self.banManager.getBanTotal())]
if flavor != "short": if flavor != "short":
ret += [("Banned IP list", banned)] ret += [("Banned IP list", banned)]
if flavor == "cymru": if flavor == "cymru":
cymru_info = self.__banManager.getBanListExtendedCymruInfo() cymru_info = self.banManager.getBanListExtendedCymruInfo()
ret += \ ret += \
[("Banned ASN list", self.__banManager.geBanListExtendedASN(cymru_info)), [("Banned ASN list", self.banManager.geBanListExtendedASN(cymru_info)),
("Banned Country list", self.__banManager.geBanListExtendedCountry(cymru_info)), ("Banned Country list", self.banManager.geBanListExtendedCountry(cymru_info)),
("Banned RIR list", self.__banManager.geBanListExtendedRIR(cymru_info))] ("Banned RIR list", self.banManager.geBanListExtendedRIR(cymru_info))]
return ret return ret

View File

@ -104,7 +104,11 @@ def commitandrollback(f):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
with self._lock: # Threading lock with self._lock: # Threading lock
with self._db: # Auto commit and rollback on exception with self._db: # Auto commit and rollback on exception
return f(self, self._db.cursor(), *args, **kwargs) cur = self._db.cursor()
try:
return f(self, cur, *args, **kwargs)
finally:
cur.close()
return wrapper return wrapper
@ -253,7 +257,7 @@ class Fail2BanDb(object):
self.repairDB() self.repairDB()
else: else:
version = cur.fetchone()[0] version = cur.fetchone()[0]
if version < Fail2BanDb.__version__: if version != Fail2BanDb.__version__:
newversion = self.updateDb(version) newversion = self.updateDb(version)
if newversion == Fail2BanDb.__version__: if newversion == Fail2BanDb.__version__:
logSys.warning( "Database updated from '%r' to '%r'", logSys.warning( "Database updated from '%r' to '%r'",
@ -301,9 +305,11 @@ class Fail2BanDb(object):
try: try:
# backup # backup
logSys.info("Trying to repair database %s", self._dbFilename) logSys.info("Trying to repair database %s", self._dbFilename)
if not os.path.isfile(self._dbBackupFilename):
shutil.move(self._dbFilename, self._dbBackupFilename) shutil.move(self._dbFilename, self._dbBackupFilename)
logSys.info(" Database backup created: %s", self._dbBackupFilename) logSys.info(" Database backup created: %s", self._dbBackupFilename)
elif os.path.isfile(self._dbFilename):
os.remove(self._dbFilename)
# first try to repair using dump/restore in order # first try to repair using dump/restore in order
Utils.executeCmd((r"""f2b_db=$0; f2b_dbbk=$1; sqlite3 "$f2b_dbbk" ".dump" | sqlite3 "$f2b_db" """, Utils.executeCmd((r"""f2b_db=$0; f2b_dbbk=$1; sqlite3 "$f2b_dbbk" ".dump" | sqlite3 "$f2b_db" """,
self._dbFilename, self._dbBackupFilename)) self._dbFilename, self._dbBackupFilename))
@ -415,7 +421,7 @@ class Fail2BanDb(object):
logSys.error("Failed to upgrade database '%s': %s", logSys.error("Failed to upgrade database '%s': %s",
self._dbFilename, e.args[0], self._dbFilename, e.args[0],
exc_info=logSys.getEffectiveLevel() <= 10) exc_info=logSys.getEffectiveLevel() <= 10)
raise self.repairDB()
@commitandrollback @commitandrollback
def addJail(self, cur, jail): def addJail(self, cur, jail):
@ -502,7 +508,7 @@ class Fail2BanDb(object):
except TypeError: except TypeError:
firstLineMD5 = None firstLineMD5 = None
if not firstLineMD5 and (pos or md5): if firstLineMD5 is None and (pos or md5 is not None):
cur.execute( cur.execute(
"INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) " "INSERT OR REPLACE INTO logs(jail, path, firstlinemd5, lastfilepos) "
"VALUES(?, ?, ?, ?)", (jail.name, name, md5, pos)) "VALUES(?, ?, ?, ?)", (jail.name, name, md5, pos))
@ -599,7 +605,7 @@ class Fail2BanDb(object):
ticket : BanTicket ticket : BanTicket
Ticket of the ban to be added. Ticket of the ban to be added.
""" """
ip = str(ticket.getIP()) ip = str(ticket.getID())
try: try:
del self._bansMergedCache[(ip, jail)] del self._bansMergedCache[(ip, jail)]
except KeyError: except KeyError:
@ -789,7 +795,6 @@ class Fail2BanDb(object):
queryArgs.append(fromtime) queryArgs.append(fromtime)
if overalljails or jail is None: if overalljails or jail is None:
query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1" query += " GROUP BY ip ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor()
# repack iterator as long as in lock: # repack iterator as long as in lock:
return list(cur.execute(query, queryArgs)) return list(cur.execute(query, queryArgs))
@ -812,11 +817,9 @@ class Fail2BanDb(object):
query += " GROUP BY ip ORDER BY ip, timeofban DESC" query += " GROUP BY ip ORDER BY ip, timeofban DESC"
else: else:
query += " ORDER BY timeofban DESC LIMIT 1" query += " ORDER BY timeofban DESC LIMIT 1"
cur = self._db.cursor()
return cur.execute(query, queryArgs) return cur.execute(query, queryArgs)
@commitandrollback def getCurrentBans(self, jail=None, ip=None, forbantime=None, fromtime=None,
def getCurrentBans(self, cur, jail=None, ip=None, forbantime=None, fromtime=None,
correctBanTime=True, maxmatches=None correctBanTime=True, maxmatches=None
): ):
"""Reads tickets (with merged info) currently affected from ban from the database. """Reads tickets (with merged info) currently affected from ban from the database.
@ -828,6 +831,8 @@ class Fail2BanDb(object):
(and therefore endOfBan) of the ticket (normally it is ban-time of jail as maximum) (and therefore endOfBan) of the ticket (normally it is ban-time of jail as maximum)
for all tickets with ban-time greater (or persistent). for all tickets with ban-time greater (or persistent).
""" """
cur = self._db.cursor()
try:
if fromtime is None: if fromtime is None:
fromtime = MyTime.time() fromtime = MyTime.time()
tickets = [] tickets = []
@ -837,9 +842,11 @@ class Fail2BanDb(object):
# don't change if persistent allowed: # don't change if persistent allowed:
if correctBanTime == -1: correctBanTime = None if correctBanTime == -1: correctBanTime = None
for ticket in self._getCurrentBans(cur, jail=jail, ip=ip, with self._lock:
bans = self._getCurrentBans(cur, jail=jail, ip=ip,
forbantime=forbantime, fromtime=fromtime forbantime=forbantime, fromtime=fromtime
): )
for ticket in bans:
# can produce unpack error (database may return sporadical wrong-empty row): # can produce unpack error (database may return sporadical wrong-empty row):
try: try:
banip, timeofban, bantime, bancount, data = ticket banip, timeofban, bantime, bancount, data = ticket
@ -879,6 +886,8 @@ class Fail2BanDb(object):
ticket.setBanCount(bancount) ticket.setBanCount(bancount)
if ip is not None: return ticket if ip is not None: return ticket
tickets.append(ticket) tickets.append(ticket)
finally:
cur.close()
return tickets return tickets

View File

@ -35,7 +35,7 @@ from ..helpers import getLogger
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = getLogger(__name__) logSys = getLogger(__name__)
logLevel = 6 logLevel = 5
RE_DATE_PREMATCH = re.compile(r"(?<!\\)\{DATE\}", re.IGNORECASE) RE_DATE_PREMATCH = re.compile(r"(?<!\\)\{DATE\}", re.IGNORECASE)
DD_patternCache = Utils.Cache(maxCount=1000, maxTime=60*60) DD_patternCache = Utils.Cache(maxCount=1000, maxTime=60*60)
@ -365,10 +365,10 @@ class DateDetector(object):
# with space or some special char), otherwise possible collision/pattern switch: # with space or some special char), otherwise possible collision/pattern switch:
if (( if ((
line[distance-1:distance] == self.__lastPos[1] or line[distance-1:distance] == self.__lastPos[1] or
(line[distance] == self.__lastPos[2] and not self.__lastPos[2].isalnum()) (line[distance:distance+1] == self.__lastPos[2] and not self.__lastPos[2].isalnum())
) and ( ) and (
line[endpos:endpos+1] == self.__lastEndPos[2] or line[endpos:endpos+1] == self.__lastEndPos[2] or
(line[endpos-1] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum()) (line[endpos-1:endpos] == self.__lastEndPos[1] and not self.__lastEndPos[1].isalnum())
)): )):
# search in line part only: # search in line part only:
log(logLevel-1, " boundaries are correct, search in part %r", line[distance:endpos]) log(logLevel-1, " boundaries are correct, search in part %r", line[distance:endpos])

View File

@ -35,6 +35,7 @@ logSys = getLogger(__name__)
# check already grouped contains "(", but ignores char "\(" and conditional "(?(id)...)": # check already grouped contains "(", but ignores char "\(" and conditional "(?(id)...)":
RE_GROUPED = re.compile(r'(?<!(?:\(\?))(?<!\\)\((?!\?)') RE_GROUPED = re.compile(r'(?<!(?:\(\?))(?<!\\)\((?!\?)')
RE_GROUP = ( re.compile(r'^((?:\(\?\w+\))?\^?(?:\(\?\w+\))?)(.*?)(\$?)$'), r"\1(\2)\3" ) RE_GROUP = ( re.compile(r'^((?:\(\?\w+\))?\^?(?:\(\?\w+\))?)(.*?)(\$?)$'), r"\1(\2)\3" )
RE_GLOBALFLAGS = re.compile(r'((?:^|(?!<\\))\(\?[a-z]+\))')
RE_EXLINE_NO_BOUNDS = re.compile(r'^\{UNB\}') RE_EXLINE_NO_BOUNDS = re.compile(r'^\{UNB\}')
RE_EXLINE_BOUND_BEG = re.compile(r'^\{\^LN-BEG\}') RE_EXLINE_BOUND_BEG = re.compile(r'^\{\^LN-BEG\}')
@ -110,6 +111,11 @@ class DateTemplate(object):
# because it may be very slow in negative case (by long log-lines not matching pattern) # because it may be very slow in negative case (by long log-lines not matching pattern)
regex = regex.strip() regex = regex.strip()
# cut global flags like (?iu) from RE in order to pre-set it after processing:
gf = RE_GLOBALFLAGS.search(regex)
if gf:
regex = RE_GLOBALFLAGS.sub('', regex, count=1)
# check word boundaries needed:
boundBegin = wordBegin and not RE_NO_WRD_BOUND_BEG.search(regex) boundBegin = wordBegin and not RE_NO_WRD_BOUND_BEG.search(regex)
boundEnd = wordEnd and not RE_NO_WRD_BOUND_END.search(regex) boundEnd = wordEnd and not RE_NO_WRD_BOUND_END.search(regex)
# if no group add it now, should always have a group(1): # if no group add it now, should always have a group(1):
@ -135,8 +141,10 @@ class DateTemplate(object):
self.flags |= DateTemplate.LINE_END self.flags |= DateTemplate.LINE_END
# remove possible special pattern "**" in front and end of regex: # remove possible special pattern "**" in front and end of regex:
regex = RE_DEL_WRD_BOUNDS[0].sub(RE_DEL_WRD_BOUNDS[1], regex) regex = RE_DEL_WRD_BOUNDS[0].sub(RE_DEL_WRD_BOUNDS[1], regex)
if gf: # restore global flags:
regex = gf.group(1) + regex
self._regex = regex self._regex = regex
logSys.log(7, ' constructed regex %s', regex) logSys.log(4, ' constructed regex %s', regex)
self._cRegex = None self._cRegex = None
regex = property(getRegex, setRegex, doc= regex = property(getRegex, setRegex, doc=
@ -159,6 +167,7 @@ class DateTemplate(object):
""" """
if not self._cRegex: if not self._cRegex:
self._compileRegex() self._compileRegex()
logSys.log(4, " search %s", self.regex)
dateMatch = self._cRegex.search(line, *args); # pos, endpos dateMatch = self._cRegex.search(line, *args); # pos, endpos
if dateMatch: if dateMatch:
self.hits += 1 self.hits += 1

View File

@ -127,9 +127,10 @@ class FailManager:
return len(self.__failList) return len(self.__failList)
def cleanup(self, time): def cleanup(self, time):
time -= self.__maxTime
with self.__lock: with self.__lock:
todelete = [fid for fid,item in self.__failList.iteritems() \ todelete = [fid for fid,item in self.__failList.iteritems() \
if item.getTime() + self.__maxTime <= time] if item.getTime() <= time]
if len(todelete) == len(self.__failList): if len(todelete) == len(self.__failList):
# remove all: # remove all:
self.__failList = dict() self.__failList = dict()
@ -143,7 +144,7 @@ class FailManager:
else: else:
# create new dictionary without items to be deleted: # create new dictionary without items to be deleted:
self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \ self.__failList = dict((fid,item) for fid,item in self.__failList.iteritems() \
if item.getTime() + self.__maxTime > time) if item.getTime() > time)
self.__bgSvc.service() self.__bgSvc.service()
def delFailure(self, fid): def delFailure(self, fid):

View File

@ -91,6 +91,13 @@ R_MAP = {
"port": "fport", "port": "fport",
} }
# map global flags like ((?i)xxx) or (?:(?i)xxx) to local flags (?i:xxx) if supported by RE-engine in this python version:
try:
re.search("^re(?i:val)$", "reVAL")
R_GLOB2LOCFLAGS = ( re.compile(r"(?<!\\)\((?:\?:)?(\(\?[a-z]+)\)"), r"\1:" )
except:
R_GLOB2LOCFLAGS = ()
def mapTag2Opt(tag): def mapTag2Opt(tag):
tag = tag.lower() tag = tag.lower()
return R_MAP.get(tag, tag) return R_MAP.get(tag, tag)
@ -128,6 +135,9 @@ class Regex:
# #
if regex.lstrip() == '': if regex.lstrip() == '':
raise RegexException("Cannot add empty regex") raise RegexException("Cannot add empty regex")
# special handling wrapping global flags to local flags:
if R_GLOB2LOCFLAGS:
regex = R_GLOB2LOCFLAGS[0].sub(R_GLOB2LOCFLAGS[1], regex)
try: try:
self._regexObj = re.compile(regex, re.MULTILINE if multiline else 0) self._regexObj = re.compile(regex, re.MULTILINE if multiline else 0)
self._regex = regex self._regex = regex
@ -147,9 +157,9 @@ class Regex:
self._tupleValues.sort() self._tupleValues.sort()
self._altValues = self._altValues if len(self._altValues) else None self._altValues = self._altValues if len(self._altValues) else None
self._tupleValues = self._tupleValues if len(self._tupleValues) else None self._tupleValues = self._tupleValues if len(self._tupleValues) else None
except sre_constants.error: except sre_constants.error as e:
raise RegexException("Unable to compile regular expression '%s'" % raise RegexException("Unable to compile regular expression '%s':\n%s" %
regex) (regex, e))
# set fetch handler depending on presence of alternate (or tuple) tags: # set fetch handler depending on presence of alternate (or tuple) tags:
self.getGroups = self._getGroupsWithAlt if (self._altValues or self._tupleValues) else self._getGroups self.getGroups = self._getGroupsWithAlt if (self._altValues or self._tupleValues) else self._getGroups

View File

@ -94,6 +94,8 @@ class Filter(JailThread):
## Store last time stamp, applicable for multi-line ## Store last time stamp, applicable for multi-line
self.__lastTimeText = "" self.__lastTimeText = ""
self.__lastDate = None self.__lastDate = None
## Next service (cleanup) time
self.__nextSvcTime = -(1<<63)
## if set, treat log lines without explicit time zone to be in this time zone ## if set, treat log lines without explicit time zone to be in this time zone
self.__logtimezone = None self.__logtimezone = None
## Default or preferred encoding (to decode bytes from file or journal): ## Default or preferred encoding (to decode bytes from file or journal):
@ -103,6 +105,10 @@ class Filter(JailThread):
## Error counter (protected, so can be used in filter implementations) ## Error counter (protected, so can be used in filter implementations)
## if it reached 100 (at once), run-cycle will go idle ## if it reached 100 (at once), run-cycle will go idle
self._errors = 0 self._errors = 0
## Next time to update log or journal position in database:
self._nextUpdateTM = 0
## Pending updates (must be executed at next update time or during stop):
self._pendDBUpdates = {}
## return raw host (host is not dns): ## return raw host (host is not dns):
self.returnRawHost = False self.returnRawHost = False
## check each regex (used for test purposes): ## check each regex (used for test purposes):
@ -115,10 +121,10 @@ class Filter(JailThread):
self.checkFindTime = True self.checkFindTime = True
## shows that filter is in operation mode (processing new messages): ## shows that filter is in operation mode (processing new messages):
self.inOperation = True self.inOperation = True
## if true prevents against retarded banning in case of RC by too many failures (disabled only for test purposes):
self.banASAP = True
## Ticks counter ## Ticks counter
self.ticks = 0 self.ticks = 0
## Processed lines counter
self.procLines = 0
## Thread name: ## Thread name:
self.name="f2b/f."+self.jailName self.name="f2b/f."+self.jailName
@ -442,12 +448,23 @@ class Filter(JailThread):
def performBan(self, ip=None): def performBan(self, ip=None):
"""Performs a ban for IPs (or given ip) that are reached maxretry of the jail.""" """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: while True:
try:
ticket = self.failManager.toBan(ip) ticket = self.failManager.toBan(ip)
self.jail.putFailTicket(ticket)
except FailManagerEmpty: except FailManagerEmpty:
self.failManager.cleanup(MyTime.time()) break
self.jail.putFailTicket(ticket)
if ip: break
self.performSvc()
def performSvc(self, force=False):
"""Performs a service tasks (clean failure list)."""
tm = MyTime.time()
# avoid too early clean up:
if force or tm >= self.__nextSvcTime:
self.__nextSvcTime = tm + 5
# clean up failure list:
self.failManager.cleanup(tm)
def addAttempt(self, ip, *matches): def addAttempt(self, ip, *matches):
"""Generate a failed attempt for ip""" """Generate a failed attempt for ip"""
@ -536,7 +553,7 @@ class Filter(JailThread):
ticket = None ticket = None
if isinstance(ip, FailTicket): if isinstance(ip, FailTicket):
ticket = ip ticket = ip
ip = ticket.getIP() ip = ticket.getID()
elif not isinstance(ip, IPAddr): elif not isinstance(ip, IPAddr):
ip = IPAddr(ip) ip = IPAddr(ip)
return self._inIgnoreIPList(ip, ticket, log_ignore) return self._inIgnoreIPList(ip, ticket, log_ignore)
@ -606,6 +623,7 @@ class Filter(JailThread):
noDate = False noDate = False
if date: if date:
tupleLine = line tupleLine = line
line = "".join(line)
self.__lastTimeText = tupleLine[1] self.__lastTimeText = tupleLine[1]
self.__lastDate = date self.__lastDate = date
else: else:
@ -642,29 +660,36 @@ class Filter(JailThread):
if self.__lastDate and self.__lastDate > MyTime.time() - 60: if self.__lastDate and self.__lastDate > MyTime.time() - 60:
tupleLine = ("", self.__lastTimeText, line) tupleLine = ("", self.__lastTimeText, line)
date = self.__lastDate date = self.__lastDate
elif self.checkFindTime and self.inOperation:
date = MyTime.time()
if self.checkFindTime: if self.checkFindTime and date is not None:
# if in operation (modifications have been really found): # if in operation (modifications have been really found):
if self.inOperation: if self.inOperation:
# if weird date - we'd simulate now for timeing issue (too large deviation from now): # if weird date - we'd simulate now for timeing issue (too large deviation from now):
if (date is None or date < MyTime.time() - 60 or date > MyTime.time() + 60): delta = int(date - MyTime.time())
# log time zone issue as warning once per day: if abs(delta) > 60:
# log timing issue as warning once per day:
self._logWarnOnce("_next_simByTimeWarn", self._logWarnOnce("_next_simByTimeWarn",
("Simulate NOW in operation since found time has too large deviation %s ~ %s +/- %s", ("Detected a log entry %s %s the current time in operation mode. "
date, MyTime.time(), 60), "This looks like a %s problem. Treating such entries as if they just happened.",
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s", MyTime.seconds2str(abs(delta)), "before" if delta < 0 else "after",
"latency" if -3300 <= delta < 0 else "timezone"
),
("Please check a jail for a timing issue. Line with odd timestamp: %s",
line)) line))
# simulate now as date: # simulate now as date:
date = MyTime.time() date = MyTime.time()
self.__lastDate = date self.__lastDate = date
else: else:
# in initialization (restore) phase, if too old - ignore: # in initialization (restore) phase, if too old - ignore:
if date is not None and date < MyTime.time() - self.getFindTime(): if date < MyTime.time() - self.getFindTime():
# log time zone issue as warning once per day: # log time zone issue as warning once per day:
self._logWarnOnce("_next_ignByTimeWarn", self._logWarnOnce("_next_ignByTimeWarn",
("Ignore line since time %s < %s - %s", ("Ignoring all log entries older than %ss; these are probably" +
date, MyTime.time(), self.getFindTime()), " messages generated while fail2ban was not running.",
("Please check jail has possibly a timezone issue. Line with odd timestamp: %s", self.getFindTime()),
("Please check a jail for a timing issue. Line with odd timestamp: %s",
line)) line))
# ignore - too old (obsolete) entry: # ignore - too old (obsolete) entry:
return [] return []
@ -677,10 +702,7 @@ class Filter(JailThread):
"""Processes the line for failures and populates failManager """Processes the line for failures and populates failManager
""" """
try: try:
for element in self.processLine(line, date): for (_, ip, unixTime, fail) in self.processLine(line, date):
ip = element[1]
unixTime = element[2]
fail = element[3]
logSys.debug("Processing line with time:%s and ip:%s", logSys.debug("Processing line with time:%s and ip:%s",
unixTime, ip) unixTime, ip)
# ensure the time is not in the future, e. g. by some estimated (assumed) time: # ensure the time is not in the future, e. g. by some estimated (assumed) time:
@ -695,11 +717,15 @@ class Filter(JailThread):
attempts = self.failManager.addFailure(tick) attempts = self.failManager.addFailure(tick)
# avoid RC on busy filter (too many failures) - if attempts for IP/ID reached maxretry, # avoid RC on busy filter (too many failures) - if attempts for IP/ID reached maxretry,
# we can speedup ban, so do it as soon as possible: # we can speedup ban, so do it as soon as possible:
if self.banASAP and attempts >= self.failManager.getMaxRetry(): if attempts >= self.failManager.getMaxRetry():
self.performBan(ip) self.performBan(ip)
# report to observer - failure was found, for possibly increasing of it retry counter (asynchronous) # report to observer - failure was found, for possibly increasing of it retry counter (asynchronous)
if Observers.Main is not None: if Observers.Main is not None:
Observers.Main.add('failureFound', self.failManager, self.jail, tick) Observers.Main.add('failureFound', self.jail, tick)
self.procLines += 1
# every 100 lines check need to perform service tasks:
if self.procLines % 100 == 0:
self.performSvc()
# reset (halve) error counter (successfully processed line): # reset (halve) error counter (successfully processed line):
if self._errors: if self._errors:
self._errors //= 2 self._errors //= 2
@ -709,7 +735,7 @@ class Filter(JailThread):
# incr common error counter: # incr common error counter:
self.commonError() self.commonError()
def commonError(self): def commonError(self, reason="common", exc=None):
# incr error counter, stop processing (going idle) after 100th error : # incr error counter, stop processing (going idle) after 100th error :
self._errors += 1 self._errors += 1
# sleep a little bit (to get around time-related errors): # sleep a little bit (to get around time-related errors):
@ -768,6 +794,8 @@ class Filter(JailThread):
# be sure we've correct current state ('nofail' and 'mlfgained' only from last failure) # be sure we've correct current state ('nofail' and 'mlfgained' only from last failure)
if mlfidGroups.pop('nofail', None): nfflgs |= 4 if mlfidGroups.pop('nofail', None): nfflgs |= 4
if mlfidGroups.pop('mlfgained', None): nfflgs |= 4 if mlfidGroups.pop('mlfgained', None): nfflgs |= 4
# gained resets all pending failures (retaining users to check it later)
if nfflgs & 8: mlfidGroups.pop('mlfpending', None)
# if we had no pending failures then clear the matches (they are already provided): # if we had no pending failures then clear the matches (they are already provided):
if (nfflgs & 4) == 0 and not mlfidGroups.get('mlfpending', 0): if (nfflgs & 4) == 0 and not mlfidGroups.get('mlfpending', 0):
mlfidGroups.pop("matches", None) mlfidGroups.pop("matches", None)
@ -812,11 +840,9 @@ class Filter(JailThread):
failList = list() failList = list()
ll = logSys.getEffectiveLevel() ll = logSys.getEffectiveLevel()
returnRawHost = self.returnRawHost defcidr = IPAddr.CIDR_UNSPEC
cidr = IPAddr.CIDR_UNSPEC if self.__useDns == "raw" or self.returnRawHost:
if self.__useDns == "raw": defcidr = IPAddr.CIDR_RAW
returnRawHost = True
cidr = IPAddr.CIDR_RAW
if self.__lineBufferSize > 1: if self.__lineBufferSize > 1:
self.__lineBuffer.append(tupleLine) self.__lineBuffer.append(tupleLine)
@ -879,7 +905,8 @@ class Filter(JailThread):
if not self.checkAllRegex or self.__lineBufferSize > 1: if not self.checkAllRegex or self.__lineBufferSize > 1:
self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None self.__lineBuffer, buf = failRegex.getUnmatchedTupleLines(), None
# merge data if multi-line failure: # merge data if multi-line failure:
raw = returnRawHost cidr = defcidr
raw = (defcidr == IPAddr.CIDR_RAW)
if preGroups: if preGroups:
currFail, fail = fail, preGroups.copy() currFail, fail = fail, preGroups.copy()
fail.update(currFail) fail.update(currFail)
@ -898,49 +925,50 @@ class Filter(JailThread):
# failure-id: # failure-id:
fid = fail.get('fid') fid = fail.get('fid')
# ip-address or host: # ip-address or host:
host = fail.get('ip4') ip = fail.get('ip4')
if host is not None: if ip is not None:
cidr = int(fail.get('cidr') or IPAddr.FAM_IPv4) cidr = int(fail.get('cidr') or IPAddr.FAM_IPv4)
raw = True raw = True
else: else:
host = fail.get('ip6') ip = fail.get('ip6')
if host is not None: if ip is not None:
cidr = int(fail.get('cidr') or IPAddr.FAM_IPv6) cidr = int(fail.get('cidr') or IPAddr.FAM_IPv6)
raw = True raw = True
if host is None: else:
host = fail.get('dns') ip = fail.get('dns')
if host is None: if ip is None:
# first try to check we have mlfid case (cache connection id): # first try to check we have mlfid case (cache connection id):
if fid is None and mlfid is None: if fid is None and mlfid is None:
# if no failure-id also (obscure case, wrong regex), throw error inside getFailID: # if no failure-id also (obscure case, wrong regex), throw error inside getFailID:
fid = failRegex.getFailID() fid = failRegex.getFailID()
host = fid ip = fid
cidr = IPAddr.CIDR_RAW
raw = True raw = True
# if mlfid case (not failure): # if mlfid case (not failure):
if host is None: if ip is None:
if ll <= 7: logSys.log(7, "No failure-id by mlfid %r in regex %s: %s", if ll <= 7: logSys.log(7, "No failure-id by mlfid %r in regex %s: %s",
mlfid, failRegexIndex, fail.get('mlfforget', "waiting for identifier")) mlfid, failRegexIndex, fail.get('mlfforget', "waiting for identifier"))
fail['mlfpending'] = 1; # mark failure is pending fail['mlfpending'] = 1; # mark failure is pending
if not self.checkAllRegex and self.ignorePending: return failList if not self.checkAllRegex and self.ignorePending: return failList
ips = [None] fids = [None]
# if raw - add single ip or failure-id, # if raw - add single ip or failure-id,
# otherwise expand host to multiple ips using dns (or ignore it if not valid): # otherwise expand host to multiple ips using dns (or ignore it if not valid):
elif raw: elif raw:
ip = IPAddr(host, cidr) # check ip/host equal failure-id, if not - failure with complex id:
# check host equal failure-id, if not - failure with complex id: if fid is None or fid == ip:
if fid is not None and fid != host: fid = IPAddr(ip, cidr)
ip = IPAddr(fid, IPAddr.CIDR_RAW) else:
ips = [ip] fail['ip'] = IPAddr(ip, cidr)
fid = IPAddr(fid, defcidr)
fids = [fid]
# otherwise, try to use dns conversion: # otherwise, try to use dns conversion:
else: else:
ips = DNSUtils.textToIp(host, self.__useDns) fids = DNSUtils.textToIp(ip, self.__useDns)
# if checkAllRegex we must make a copy (to be sure next RE doesn't change merged/cached failure): # if checkAllRegex we must make a copy (to be sure next RE doesn't change merged/cached failure):
if self.checkAllRegex and mlfid is not None: if self.checkAllRegex and mlfid is not None:
fail = fail.copy() fail = fail.copy()
# append failure with match to the list: # append failure with match to the list:
for ip in ips: for fid in fids:
failList.append([failRegexIndex, ip, date, fail]) failList.append([failRegexIndex, fid, date, fail])
if not self.checkAllRegex: if not self.checkAllRegex:
break break
except RegexException as e: # pragma: no cover - unsure if reachable except RegexException as e: # pragma: no cover - unsure if reachable
@ -1002,9 +1030,6 @@ class FileFilter(Filter):
log = self.__logs.pop(path) log = self.__logs.pop(path)
except KeyError: except KeyError:
return return
db = self.jail.database
if db is not None:
db.updateLog(self.jail, log)
logSys.info("Removed logfile: %r", path) logSys.info("Removed logfile: %r", path)
self._delLogPath(path) self._delLogPath(path)
return return
@ -1068,6 +1093,7 @@ class FileFilter(Filter):
# is created and is added to the FailManager. # is created and is added to the FailManager.
def getFailures(self, filename, inOperation=None): def getFailures(self, filename, inOperation=None):
if self.idle: return False
log = self.getLog(filename) log = self.getLog(filename)
if log is None: if log is None:
logSys.error("Unable to get failures in %s", filename) logSys.error("Unable to get failures in %s", filename)
@ -1113,19 +1139,25 @@ class FileFilter(Filter):
while not self.idle: while not self.idle:
line = log.readline() line = log.readline()
if not self.active: break; # jail has been stopped if not self.active: break; # jail has been stopped
if not line: if line is None:
# The jail reached the bottom, simply set in operation for this log # The jail reached the bottom, simply set in operation for this log
# (since we are first time at end of file, growing is only possible after modifications): # (since we are first time at end of file, growing is only possible after modifications):
log.inOperation = True log.inOperation = True
break break
# acquire in operation from log and process: # acquire in operation from log and process:
self.inOperation = inOperation if inOperation is not None else log.inOperation self.inOperation = inOperation if inOperation is not None else log.inOperation
self.processLineAndAdd(line.rstrip('\r\n')) self.processLineAndAdd(line)
finally: finally:
log.close() log.close()
db = self.jail.database if self.jail.database is not None:
if db is not None: self._pendDBUpdates[log] = 1
db.updateLog(self.jail, log) if (
self.ticks % 100 == 0
or MyTime.time() >= self._nextUpdateTM
or not self.active
):
self._updateDBPending()
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
return True return True
## ##
@ -1137,6 +1169,8 @@ class FileFilter(Filter):
if logSys.getEffectiveLevel() <= logging.DEBUG: if logSys.getEffectiveLevel() <= logging.DEBUG:
logSys.debug("Seek to find time %s (%s), file size %s", date, logSys.debug("Seek to find time %s (%s), file size %s", date,
MyTime.time2str(date), fs) MyTime.time2str(date), fs)
if not fs:
return
minp = container.getPos() minp = container.getPos()
maxp = fs maxp = fs
tryPos = minp tryPos = minp
@ -1160,8 +1194,8 @@ class FileFilter(Filter):
dateTimeMatch = None dateTimeMatch = None
nextp = None nextp = None
while True: while True:
line = container.readline() line = container.readline(False)
if not line: if line is None:
break break
(timeMatch, template) = self.dateDetector.matchTime(line) (timeMatch, template) = self.dateDetector.matchTime(line)
if timeMatch: if timeMatch:
@ -1225,12 +1259,33 @@ class FileFilter(Filter):
ret.append(("File list", path)) ret.append(("File list", path))
return ret return ret
def stop(self): def _updateDBPending(self):
"""Stop monitoring of log-file(s) """Apply pending updates (log position) to database.
""" """
db = self.jail.database
while True:
try:
log, args = self._pendDBUpdates.popitem()
except KeyError:
break
db.updateLog(self.jail, log)
def onStop(self):
"""Stop monitoring of log-file(s). Invoked after run method.
"""
# ensure positions of pending logs are up-to-date:
if self._pendDBUpdates and self.jail.database:
self._updateDBPending()
# stop files monitoring: # stop files monitoring:
for path in self.__logs.keys(): for path in self.__logs.keys():
self.delLogPath(path) self.delLogPath(path)
def stop(self):
"""Stop filter
"""
# normally onStop will be called automatically in thread after its run ends,
# but for backwards compatibilities we'll invoke it in caller of stop method.
self.onStop()
# stop thread: # stop thread:
super(Filter, self).stop() super(Filter, self).stop()
@ -1258,34 +1313,56 @@ except ImportError: # pragma: no cover
class FileContainer: class FileContainer:
def __init__(self, filename, encoding, tail=False): def __init__(self, filename, encoding, tail=False, doOpen=False):
self.__filename = filename self.__filename = filename
self.waitForLineEnd = True
self.setEncoding(encoding) self.setEncoding(encoding)
self.__tail = tail self.__tail = tail
self.__handler = None self.__handler = None
self.__pos = 0
self.__pos4hash = 0
self.__hash = ''
self.__hashNextTime = time.time() + 30
# Try to open the file. Raises an exception if an error occurred. # Try to open the file. Raises an exception if an error occurred.
handler = open(filename, 'rb') handler = open(filename, 'rb')
if doOpen: # fail2ban-regex only (don't need to reopen it and check for rotation)
self.__handler = handler
return
try:
stats = os.fstat(handler.fileno()) stats = os.fstat(handler.fileno())
self.__ino = stats.st_ino self.__ino = stats.st_ino
try: if stats.st_size:
firstLine = handler.readline() firstLine = handler.readline()
# first line available and contains new-line:
if firstLine != firstLine.rstrip(b'\r\n'):
# Computes the MD5 of the first line. # Computes the MD5 of the first line.
self.__hash = md5sum(firstLine).hexdigest() self.__hash = md5sum(firstLine).hexdigest()
# Start at the beginning of file if tail mode is off. # if tail mode scroll to the end of file
if tail: if tail:
handler.seek(0, 2) handler.seek(0, 2)
self.__pos = handler.tell() self.__pos = handler.tell()
else:
self.__pos = 0
finally: finally:
handler.close() handler.close()
## shows that log is in operation mode (expecting new messages only from here): ## shows that log is in operation mode (expecting new messages only from here):
self.inOperation = tail self.inOperation = tail
def __hash__(self):
return hash(self.__filename)
def __eq__(self, other):
return (id(self) == id(other) or
self.__filename == (other.__filename if isinstance(other, FileContainer) else other)
)
def __repr__(self):
return 'file-log:'+self.__filename
def getFileName(self): def getFileName(self):
return self.__filename return self.__filename
def getFileSize(self): def getFileSize(self):
h = self.__handler
if h is not None:
stats = os.fstat(h.fileno())
return stats.st_size
return os.path.getsize(self.__filename); return os.path.getsize(self.__filename);
def setEncoding(self, encoding): def setEncoding(self, encoding):
@ -1304,38 +1381,54 @@ class FileContainer:
def setPos(self, value): def setPos(self, value):
self.__pos = value self.__pos = value
def open(self): def open(self, forcePos=None):
self.__handler = open(self.__filename, 'rb') h = open(self.__filename, 'rb')
try:
# Set the file descriptor to be FD_CLOEXEC # Set the file descriptor to be FD_CLOEXEC
fd = self.__handler.fileno() fd = h.fileno()
flags = fcntl.fcntl(fd, fcntl.F_GETFD) flags = fcntl.fcntl(fd, fcntl.F_GETFD)
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
myHash = self.__hash
# Stat the file before even attempting to read it # Stat the file before even attempting to read it
stats = os.fstat(self.__handler.fileno()) stats = os.fstat(h.fileno())
if not stats.st_size: rotflg = stats.st_size < self.__pos or stats.st_ino != self.__ino
# yoh: so it is still an empty file -- nothing should be if rotflg or not len(myHash) or time.time() > self.__hashNextTime:
# read from it yet myHash = ''
# print "D: no content -- return" firstLine = h.readline()
return False # Computes the MD5 of the first line (if it is complete)
firstLine = self.__handler.readline() if firstLine != firstLine.rstrip(b'\r\n'):
# Computes the MD5 of the first line.
myHash = md5sum(firstLine).hexdigest() myHash = md5sum(firstLine).hexdigest()
## print "D: fn=%s hashes=%s/%s inos=%s/%s pos=%s rotate=%s" % ( self.__hashNextTime = time.time() + 30
## self.__filename, self.__hash, myHash, stats.st_ino, self.__ino, self.__pos, elif stats.st_size == self.__pos:
## self.__hash != myHash or self.__ino != stats.st_ino) myHash = self.__hash
## sys.stdout.flush() # Compare size, hash and inode
# Compare hash and inode if rotflg or myHash != self.__hash:
if self.__hash != myHash or self.__ino != stats.st_ino: if self.__hash != '':
logSys.log(logging.MSG, "Log rotation detected for %s", self.__filename) logSys.log(logging.MSG, "Log rotation detected for %s, reason: %r", self.__filename,
self.__hash = myHash (stats.st_size, self.__pos, stats.st_ino, self.__ino, myHash, self.__hash))
self.__ino = stats.st_ino self.__ino = stats.st_ino
self.__pos = 0 self.__pos = 0
self.__hash = myHash
# if nothing to read from file yet (empty or no new data):
if forcePos is not None:
self.__pos = forcePos
elif stats.st_size <= self.__pos:
return False
# Sets the file pointer to the last position. # Sets the file pointer to the last position.
self.__handler.seek(self.__pos) h.seek(self.__pos)
# leave file open (to read content):
self.__handler = h; h = None
finally:
# close (no content or error only)
if h:
h.close(); h = None
return True return True
def seek(self, offs, endLine=True): def seek(self, offs, endLine=True):
h = self.__handler h = self.__handler
if h is None:
self.open(offs)
h = self.__handler
# seek to given position # seek to given position
h.seek(offs, 0) h.seek(offs, 0)
# goto end of next line # goto end of next line
@ -1353,6 +1446,9 @@ class FileContainer:
try: try:
return line.decode(enc, 'strict') return line.decode(enc, 'strict')
except (UnicodeDecodeError, UnicodeEncodeError) as e: except (UnicodeDecodeError, UnicodeEncodeError) as e:
# avoid warning if got incomplete end of line (e. g. '\n' in "...[0A" followed by "00]..." for utf-16le:
if (e.end == len(line) and line[e.start] in b'\r\n'):
return line[0:e.start].decode(enc, 'replace')
global _decode_line_warn global _decode_line_warn
lev = 7 lev = 7
if not _decode_line_warn.get(filename, 0): if not _decode_line_warn.get(filename, 0):
@ -1361,29 +1457,85 @@ class FileContainer:
logSys.log(lev, logSys.log(lev,
"Error decoding line from '%s' with '%s'.", filename, enc) "Error decoding line from '%s' with '%s'.", filename, enc)
if logSys.getEffectiveLevel() <= lev: if logSys.getEffectiveLevel() <= lev:
logSys.log(lev, "Consider setting logencoding=utf-8 (or another appropriate" logSys.log(lev,
" encoding) for this jail. Continuing" "Consider setting logencoding to appropriate encoding for this jail. "
" to process line ignoring invalid characters: %r", "Continuing to process line ignoring invalid characters: %r",
line) line)
# decode with replacing error chars: # decode with replacing error chars:
line = line.decode(enc, 'replace') line = line.decode(enc, 'replace')
return line return line
def readline(self): def readline(self, complete=True):
"""Read line from file
In opposite to pythons readline it doesn't return new-line,
so returns either the line if line is complete (and complete=True) or None
if line is not complete (and complete=True) or there is no content to read.
If line is complete (and complete is True), it also shift current known
position to begin of next line.
Also it is safe against interim new-line bytes (e. g. part of multi-byte char)
in given encoding.
"""
if self.__handler is None: if self.__handler is None:
return "" return ""
return FileContainer.decode_line( # read raw bytes up to \n char:
self.getFileName(), self.getEncoding(), self.__handler.readline()) b = self.__handler.readline()
if not b:
return None
bl = len(b)
# convert to log-encoding (new-line char could disappear if it is part of multi-byte sequence):
r = FileContainer.decode_line(
self.getFileName(), self.getEncoding(), b)
# trim new-line at end and check the line was written complete (contains a new-line):
l = r.rstrip('\r\n')
if complete:
if l == r:
# try to fill buffer in order to find line-end in log encoding:
fnd = 0
while 1:
r = self.__handler.readline()
if not r:
break
b += r
bl += len(r)
# convert to log-encoding:
r = FileContainer.decode_line(
self.getFileName(), self.getEncoding(), b)
# ensure new-line is not in the middle (buffered 2 strings, e. g. in utf-16le it is "...[0A"+"00]..."):
e = r.find('\n')
if e >= 0 and e != len(r)-1:
l, r = r[0:e], r[0:e+1]
# back to bytes and get offset to seek after NL:
r = r.encode(self.getEncoding(), 'replace')
self.__handler.seek(-bl+len(r), 1)
return l
# trim new-line at end and check the line was written complete (contains a new-line):
l = r.rstrip('\r\n')
if l != r:
return l
if self.waitForLineEnd:
# not fulfilled - seek back and return:
self.__handler.seek(-bl, 1)
return None
return l
def close(self): def close(self):
if not self.__handler is None: if self.__handler is not None:
# Saves the last position. # Saves the last real position.
self.__pos = self.__handler.tell() self.__pos = self.__handler.tell()
# Closes the file. # Closes the file.
self.__handler.close() self.__handler.close()
self.__handler = None self.__handler = None
## print "D: Closed %s with pos %d" % (handler, self.__pos)
## sys.stdout.flush() def __iter__(self):
return self
def next(self):
line = self.readline()
if line is None:
self.close()
raise StopIteration
return line
_decode_line_warn = Utils.Cache(maxCount=1000, maxTime=24*60*60); _decode_line_warn = Utils.Cache(maxCount=1000, maxTime=24*60*60);

View File

@ -55,7 +55,6 @@ class FilterGamin(FileFilter):
def __init__(self, jail): def __init__(self, jail):
FileFilter.__init__(self, jail) FileFilter.__init__(self, jail)
self.__modified = False
# Gamin monitor # Gamin monitor
self.monitor = gamin.WatchMonitor() self.monitor = gamin.WatchMonitor()
fd = self.monitor.get_fd() fd = self.monitor.get_fd()
@ -67,21 +66,9 @@ class FilterGamin(FileFilter):
logSys.log(4, "Got event: " + repr(event) + " for " + path) logSys.log(4, "Got event: " + repr(event) + " for " + path)
if event in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists): if event in (gamin.GAMCreated, gamin.GAMChanged, gamin.GAMExists):
logSys.debug("File changed: " + path) logSys.debug("File changed: " + path)
self.__modified = True
self.ticks += 1 self.ticks += 1
self._process_file(path)
def _process_file(self, path):
"""Process a given file
TODO -- RF:
this is a common logic and must be shared/provided by FileFilter
"""
self.getFailures(path) self.getFailures(path)
if not self.banASAP: # pragma: no cover
self.performBan()
self.__modified = False
## ##
# Add a log file path # Add a log file path
@ -128,6 +115,9 @@ class FilterGamin(FileFilter):
Utils.wait_for(lambda: not self.active or self._handleEvents(), Utils.wait_for(lambda: not self.active or self._handleEvents(),
self.sleeptime) self.sleeptime)
self.ticks += 1 self.ticks += 1
if self.ticks % 10 == 0:
self.performSvc()
logSys.debug("[%s] filter terminated", self.jailName) logSys.debug("[%s] filter terminated", self.jailName)
return True return True

View File

@ -27,9 +27,7 @@ __license__ = "GPL"
import os import os
import time import time
from .failmanager import FailManagerEmpty
from .filter import FileFilter from .filter import FileFilter
from .mytime import MyTime
from .utils import Utils from .utils import Utils
from ..helpers import getLogger, logging from ..helpers import getLogger, logging
@ -55,7 +53,6 @@ class FilterPoll(FileFilter):
def __init__(self, jail): def __init__(self, jail):
FileFilter.__init__(self, jail) FileFilter.__init__(self, jail)
self.__modified = False
## The time of the last modification of the file. ## The time of the last modification of the file.
self.__prevStats = dict() self.__prevStats = dict()
self.__file404Cnt = dict() self.__file404Cnt = dict()
@ -115,20 +112,17 @@ class FilterPoll(FileFilter):
break break
for filename in modlst: for filename in modlst:
self.getFailures(filename) self.getFailures(filename)
self.__modified = True
self.ticks += 1 self.ticks += 1
if self.__modified: if self.ticks % 10 == 0:
if not self.banASAP: # pragma: no cover self.performSvc()
self.performBan()
self.__modified = False
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop... if not self.active: # if not active - error by stop...
break break
logSys.error("Caught unhandled exception in main cycle: %r", e, logSys.error("Caught unhandled exception in main cycle: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# incr common error counter: # incr common error counter:
self.commonError() self.commonError("unhandled", e)
logSys.debug("[%s] filter terminated", self.jailName) logSys.debug("[%s] filter terminated", self.jailName)
return True return True

View File

@ -75,7 +75,6 @@ class FilterPyinotify(FileFilter):
def __init__(self, jail): def __init__(self, jail):
FileFilter.__init__(self, jail) FileFilter.__init__(self, jail)
self.__modified = False
# Pyinotify watch manager # Pyinotify watch manager
self.__monitor = pyinotify.WatchManager() self.__monitor = pyinotify.WatchManager()
self.__notifier = None self.__notifier = None
@ -140,9 +139,6 @@ class FilterPyinotify(FileFilter):
""" """
if not self.idle: if not self.idle:
self.getFailures(path) self.getFailures(path)
if not self.banASAP: # pragma: no cover
self.performBan()
self.__modified = False
def _addPending(self, path, reason, isDir=False): def _addPending(self, path, reason, isDir=False):
if path not in self.__pending: if path not in self.__pending:
@ -169,7 +165,7 @@ class FilterPyinotify(FileFilter):
return return
found = {} found = {}
minTime = 60 minTime = 60
for path, (retardTM, isDir) in self.__pending.iteritems(): for path, (retardTM, isDir) in list(self.__pending.items()):
if ntm - self.__pendingChkTime < retardTM: if ntm - self.__pendingChkTime < retardTM:
if minTime > retardTM: minTime = retardTM if minTime > retardTM: minTime = retardTM
continue continue
@ -192,7 +188,7 @@ class FilterPyinotify(FileFilter):
self._refreshWatcher(path, isDir=isDir) self._refreshWatcher(path, isDir=isDir)
if isDir: if isDir:
# check all files belong to this dir: # check all files belong to this dir:
for logpath in self.__watchFiles: for logpath in list(self.__watchFiles):
if logpath.startswith(path + pathsep): if logpath.startswith(path + pathsep):
# if still no file - add to pending, otherwise refresh and process: # if still no file - add to pending, otherwise refresh and process:
if not os.path.isfile(logpath): if not os.path.isfile(logpath):
@ -272,11 +268,9 @@ class FilterPyinotify(FileFilter):
def _addLogPath(self, path): def _addLogPath(self, path):
self._addFileWatcher(path) self._addFileWatcher(path)
# initial scan: # notify (wake up if in waiting):
if self.active: if self.active:
# we can execute it right now: self.__pendingMinTime = 0
self._process_file(path)
else:
# retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor): # retard until filter gets started, isDir=None signals special case: process file only (don't need to refresh monitor):
self._addPending(path, ('INITIAL', path), isDir=None) self._addPending(path, ('INITIAL', path), isDir=None)
@ -291,7 +285,7 @@ class FilterPyinotify(FileFilter):
logSys.error("Failed to remove watch on path: %s", path) logSys.error("Failed to remove watch on path: %s", path)
path_dir = dirname(path) path_dir = dirname(path)
for k in self.__watchFiles: for k in list(self.__watchFiles):
if k.startswith(path_dir + pathsep): if k.startswith(path_dir + pathsep):
path_dir = None path_dir = None
break break
@ -345,16 +339,26 @@ class FilterPyinotify(FileFilter):
self.__notifier.process_events() self.__notifier.process_events()
# wait for events / timeout: # wait for events / timeout:
notify_maxtout = self.__notify_maxtout
def __check_events(): def __check_events():
return not self.active or self.__notifier.check_events(timeout=notify_maxtout) return (
if Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime)): not self.active
or bool(self.__notifier.check_events(timeout=self.__notify_maxtout))
or (self.__pendingMinTime and self.__pending)
)
wres = Utils.wait_for(__check_events, min(self.sleeptime, self.__pendingMinTime))
if wres:
if not self.active: break if not self.active: break
if not isinstance(wres, dict):
self.__notifier.read_events() self.__notifier.read_events()
self.ticks += 1
# check pending files/dirs (logrotate ready): # check pending files/dirs (logrotate ready):
if not self.idle: if self.idle:
continue
self._checkPending() self._checkPending()
if self.ticks % 10 == 0:
self.performSvc()
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop... if not self.active: # if not active - error by stop...
@ -362,9 +366,7 @@ class FilterPyinotify(FileFilter):
logSys.error("Caught unhandled exception in main cycle: %r", e, logSys.error("Caught unhandled exception in main cycle: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# incr common error counter: # incr common error counter:
self.commonError() self.commonError("unhandled", e)
self.ticks += 1
logSys.debug("[%s] filter exited (pyinotifier)", self.jailName) logSys.debug("[%s] filter exited (pyinotifier)", self.jailName)
self.__notifier = None self.__notifier = None

View File

@ -22,7 +22,7 @@ __author__ = "Steven Hiscocks"
__copyright__ = "Copyright (c) 2013 Steven Hiscocks" __copyright__ = "Copyright (c) 2013 Steven Hiscocks"
__license__ = "GPL" __license__ = "GPL"
import datetime import os
import time import time
from distutils.version import LooseVersion from distutils.version import LooseVersion
@ -91,9 +91,14 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
try: try:
args['flags'] = int(kwargs.pop('journalflags')) args['flags'] = int(kwargs.pop('journalflags'))
except KeyError: except KeyError:
# be sure all journal types will be opened if files specified (don't set flags): # be sure all journal types will be opened if files/path specified (don't set flags):
if 'files' not in args or not len(args['files']): if ('files' not in args or not len(args['files'])) and ('path' not in args or not args['path']):
args['flags'] = 4 args['flags'] = int(os.getenv("F2B_SYSTEMD_DEFAULT_FLAGS", 4))
try:
args['namespace'] = kwargs.pop('namespace')
except KeyError:
pass
return args return args
@ -245,13 +250,17 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
logSys.log(5, "[%s] Read systemd journal entry: %s %s", self.jailName, logSys.log(5, "[%s] Read systemd journal entry: %s %s", self.jailName,
date[0], logline) date[0], logline)
## use the same type for 1st argument: ## use the same type for 1st argument:
return ((logline[:0], date[0], logline.replace('\n', '\\n')), date[1]) return ((logline[:0], date[0] + ' ', logline.replace('\n', '\\n')), date[1])
def seekToTime(self, date): def seekToTime(self, date):
if not isinstance(date, datetime.datetime): if isinstance(date, (int, long)):
date = datetime.datetime.fromtimestamp(date) date = float(date)
self.__journal.seek_realtime(date) self.__journal.seek_realtime(date)
def inOperationMode(self):
self.inOperation = True
logSys.info("[%s] Jail is in operation now (process new journal entries)", self.jailName)
## ##
# Main loop. # Main loop.
# #
@ -262,17 +271,40 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
if not self.getJournalMatch(): if not self.getJournalMatch():
logSys.notice( logSys.notice(
"Jail started without 'journalmatch' set. " "[%s] Jail started without 'journalmatch' set. "
"Jail regexs will be checked against all journal entries, " "Jail regexs will be checked against all journal entries, "
"which is not advised for performance reasons.") "which is not advised for performance reasons.", self.jailName)
# Save current cursor position (to recognize in operation mode):
logentry = None
try:
self.__journal.seek_tail()
logentry = self.__journal.get_previous()
if logentry:
self.__journal.get_next()
except OSError:
logentry = None # Reading failure, so safe to ignore
if logentry:
# Try to obtain the last known time (position of journal) # Try to obtain the last known time (position of journal)
start_time = 0 startTime = 0
if self.jail.database is not None: if self.jail.database is not None:
start_time = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0 startTime = self.jail.database.getJournalPos(self.jail, 'systemd-journal') or 0
# Seek to max(last_known_time, now - findtime) in journal # Seek to max(last_known_time, now - findtime) in journal
start_time = max( start_time, MyTime.time() - int(self.getFindTime()) ) startTime = max( startTime, MyTime.time() - int(self.getFindTime()) )
self.seekToTime(start_time) self.seekToTime(startTime)
# Not in operation while we'll read old messages ...
self.inOperation = False
# Save current time in order to check time to switch "in operation" mode
startTime = (1, MyTime.time(), logentry.get('__CURSOR'))
else:
# empty journal or no entries for current filter:
self.inOperationMode()
# seek_tail() seems to have a bug by no entries (could bypass some entries hereafter), so seek to now instead:
startTime = MyTime.time()
self.seekToTime(startTime)
# for possible future switches of in-operation mode:
startTime = (0, startTime)
# Move back one entry to ensure do not end up in dead space # Move back one entry to ensure do not end up in dead space
# if start time beyond end of journal # if start time beyond end of journal
try: try:
@ -280,6 +312,7 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
except OSError: except OSError:
pass # Reading failure, so safe to ignore pass # Reading failure, so safe to ignore
line = None
while self.active: while self.active:
# wait for records (or for timeout in sleeptime seconds): # wait for records (or for timeout in sleeptime seconds):
try: try:
@ -289,6 +322,7 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
#self.__journal.wait(self.sleeptime) != journal.NOP #self.__journal.wait(self.sleeptime) != journal.NOP
## ##
## wait for entries without sleep in intervals, because "sleeping" in journal.wait: ## wait for entries without sleep in intervals, because "sleeping" in journal.wait:
if not logentry:
Utils.wait_for(lambda: not self.active or \ Utils.wait_for(lambda: not self.active or \
self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP, self.__journal.wait(Utils.DEFAULT_SLEEP_INTERVAL) != journal.NOP,
self.sleeptime, 0.00001) self.sleeptime, 0.00001)
@ -310,27 +344,50 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG) e, exc_info=logSys.getEffectiveLevel() <= logging.DEBUG)
self.ticks += 1 self.ticks += 1
if logentry: if logentry:
line = self.formatJournalEntry(logentry) line, tm = self.formatJournalEntry(logentry)
self.processLineAndAdd(*line) # switch "in operation" mode if we'll find start entry (+ some delta):
if not self.inOperation:
if tm >= MyTime.time() - 1: # reached now (approximated):
self.inOperationMode()
elif startTime[0] == 1:
# if it reached start entry (or get read time larger than start time)
if logentry.get('__CURSOR') == startTime[2] or tm > startTime[1]:
# give the filter same time it needed to reach the start entry:
startTime = (0, MyTime.time()*2 - startTime[1])
elif tm > startTime[1]: # reached start time (approximated):
self.inOperationMode()
# process line
self.processLineAndAdd(line, tm)
self.__modified += 1 self.__modified += 1
if self.__modified >= 100: # todo: should be configurable if self.__modified >= 100: # todo: should be configurable
break break
else: else:
# "in operation" mode since we don't have messages anymore (reached end of journal):
if not self.inOperation:
self.inOperationMode()
break break
if self.__modified:
if not self.banASAP: # pragma: no cover
self.performBan()
self.__modified = 0 self.__modified = 0
if self.ticks % 10 == 0:
self.performSvc()
# update position in log (time and iso string): # update position in log (time and iso string):
if self.jail.database is not None: if self.jail.database:
self.jail.database.updateJournal(self.jail, 'systemd-journal', line[1], line[0][1]) if line:
self._pendDBUpdates['systemd-journal'] = (tm, line[1])
line = None
if self._pendDBUpdates and (
self.ticks % 100 == 0
or MyTime.time() >= self._nextUpdateTM
or not self.active
):
self._updateDBPending()
self._nextUpdateTM = MyTime.time() + Utils.DEFAULT_SLEEP_TIME * 5
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
if not self.active: # if not active - error by stop... if not self.active: # if not active - error by stop...
break break
logSys.error("Caught unhandled exception in main cycle: %r", e, logSys.error("Caught unhandled exception in main cycle: %r", e,
exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
# incr common error counter: # incr common error counter:
self.commonError() self.commonError("unhandled", e)
logSys.debug("[%s] filter terminated", self.jailName) logSys.debug("[%s] filter terminated", self.jailName)
@ -350,3 +407,22 @@ class FilterSystemd(JournalFilter): # pragma: systemd no cover
ret.append(("Journal matches", ret.append(("Journal matches",
[" + ".join(" ".join(match) for match in self.__matches)])) [" + ".join(" ".join(match) for match in self.__matches)]))
return ret return ret
def _updateDBPending(self):
"""Apply pending updates (jornal position) to database.
"""
db = self.jail.database
while True:
try:
log, args = self._pendDBUpdates.popitem()
except KeyError:
break
db.updateJournal(self.jail, log, *args)
def onStop(self):
"""Stop monitoring of journal. Invoked after run method.
"""
# ensure positions of pending logs are up-to-date:
if self._pendDBUpdates and self.jail.database:
self._updateDBPending()

View File

@ -169,27 +169,31 @@ class DNSUtils:
DNSUtils.CACHE_ipToName.set(key, name) DNSUtils.CACHE_ipToName.set(key, name)
return name return name
# key find cached own hostnames (this tuple-key cannot be used elsewhere):
_getSelfNames_key = ('self','dns')
@staticmethod @staticmethod
def getSelfNames(): def getSelfNames():
"""Get own host names of self""" """Get own host names of self"""
# try find cached own hostnames (this tuple-key cannot be used elsewhere): # try find cached own hostnames:
key = ('self','dns') names = DNSUtils.CACHE_ipToName.get(DNSUtils._getSelfNames_key)
names = DNSUtils.CACHE_ipToName.get(key)
# get it using different ways (a set with names of localhost, hostname, fully qualified): # get it using different ways (a set with names of localhost, hostname, fully qualified):
if names is None: if names is None:
names = set([ names = set([
'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True) 'localhost', DNSUtils.getHostname(False), DNSUtils.getHostname(True)
]) - set(['']) # getHostname can return '' ]) - set(['']) # getHostname can return ''
# cache and return : # cache and return :
DNSUtils.CACHE_ipToName.set(key, names) DNSUtils.CACHE_ipToName.set(DNSUtils._getSelfNames_key, names)
return names return names
# key to find cached own IPs (this tuple-key cannot be used elsewhere):
_getSelfIPs_key = ('self','ips')
@staticmethod @staticmethod
def getSelfIPs(): def getSelfIPs():
"""Get own IP addresses of self""" """Get own IP addresses of self"""
# try find cached own IPs (this tuple-key cannot be used elsewhere): # to find cached own IPs:
key = ('self','ips') ips = DNSUtils.CACHE_nameToIp.get(DNSUtils._getSelfIPs_key)
ips = DNSUtils.CACHE_nameToIp.get(key)
# get it using different ways (a set with IPs of localhost, hostname, fully qualified): # get it using different ways (a set with IPs of localhost, hostname, fully qualified):
if ips is None: if ips is None:
ips = set() ips = set()
@ -199,13 +203,30 @@ class DNSUtils:
except Exception as e: # pragma: no cover except Exception as e: # pragma: no cover
logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e) logSys.warning("Retrieving own IPs of %s failed: %s", hostname, e)
# cache and return : # cache and return :
DNSUtils.CACHE_nameToIp.set(key, ips) DNSUtils.CACHE_nameToIp.set(DNSUtils._getSelfIPs_key, ips)
return ips return ips
_IPv6IsAllowed = None
@staticmethod
def setIPv6IsAllowed(value):
DNSUtils._IPv6IsAllowed = value
logSys.debug("IPv6 is %s", ('on' if value else 'off') if value is not None else 'auto')
return value
# key to find cached value of IPv6 allowance (this tuple-key cannot be used elsewhere):
_IPv6IsAllowed_key = ('self','ipv6-allowed')
@staticmethod @staticmethod
def IPv6IsAllowed(): def IPv6IsAllowed():
# return os.path.exists("/proc/net/if_inet6") || any((':' in ip) for ip in DNSUtils.getSelfIPs()) if DNSUtils._IPv6IsAllowed is not None:
return any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs()) return DNSUtils._IPv6IsAllowed
v = DNSUtils.CACHE_nameToIp.get(DNSUtils._IPv6IsAllowed_key)
if v is not None:
return v
v = any((':' in ip.ntoa) for ip in DNSUtils.getSelfIPs())
DNSUtils.CACHE_nameToIp.set(DNSUtils._IPv6IsAllowed_key, v)
return v
## ##
@ -236,6 +257,8 @@ class IPAddr(object):
FAM_IPv6 = CIDR_RAW - socket.AF_INET6 FAM_IPv6 = CIDR_RAW - socket.AF_INET6
def __new__(cls, ipstr, cidr=CIDR_UNSPEC): def __new__(cls, ipstr, cidr=CIDR_UNSPEC):
if cidr == IPAddr.CIDR_UNSPEC and isinstance(ipstr, (tuple, list)):
cidr = IPAddr.CIDR_RAW
if cidr == IPAddr.CIDR_RAW: # don't cache raw if cidr == IPAddr.CIDR_RAW: # don't cache raw
ip = super(IPAddr, cls).__new__(cls) ip = super(IPAddr, cls).__new__(cls)
ip.__init(ipstr, cidr) ip.__init(ipstr, cidr)

View File

@ -295,7 +295,7 @@ class Jail(object):
): ):
try: try:
#logSys.debug('restored ticket: %s', ticket) #logSys.debug('restored ticket: %s', ticket)
if self.filter.inIgnoreIPList(ticket.getIP(), log_ignore=True): continue if self.filter.inIgnoreIPList(ticket.getID(), log_ignore=True): continue
# mark ticked was restored from database - does not put it again into db: # mark ticked was restored from database - does not put it again into db:
ticket.restored = True ticket.restored = True
# correct start time / ban time (by the same end of ban): # correct start time / ban time (by the same end of ban):

View File

@ -22,6 +22,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from threading import Lock from threading import Lock
try:
from collections.abc import Mapping
except ImportError:
from collections import Mapping from collections import Mapping
from ..exceptions import DuplicateJailException, UnknownJailException from ..exceptions import DuplicateJailException, UnknownJailException

View File

@ -67,6 +67,8 @@ class JailThread(Thread):
def run_with_except_hook(*args, **kwargs): def run_with_except_hook(*args, **kwargs):
try: try:
run(*args, **kwargs) run(*args, **kwargs)
# call on stop callback to do some finalizations:
self.onStop()
except Exception as e: except Exception as e:
# avoid very sporadic error "'NoneType' object has no attribute 'exc_info'" (https://bugs.python.org/issue7336) # avoid very sporadic error "'NoneType' object has no attribute 'exc_info'" (https://bugs.python.org/issue7336)
# only extremely fast systems are affected ATM (2.7 / 3.x), if thread ends nothing is available here. # only extremely fast systems are affected ATM (2.7 / 3.x), if thread ends nothing is available here.
@ -97,6 +99,12 @@ class JailThread(Thread):
self.active = True self.active = True
super(JailThread, self).start() super(JailThread, self).start()
@abstractmethod
def onStop(self): # pragma: no cover - absract
"""Abstract - Called when thread ends (after run).
"""
pass
def stop(self): def stop(self):
"""Sets `active` property to False, to flag run method to return. """Sets `active` property to False, to flag run method to return.
""" """

View File

@ -174,3 +174,62 @@ class MyTime:
val = rexp.sub(rpl, val) val = rexp.sub(rpl, val)
val = MyTime._str2sec_fini.sub(r"\1+\2", val) val = MyTime._str2sec_fini.sub(r"\1+\2", val)
return eval(val) return eval(val)
class seconds2str():
"""Converts seconds to string on demand (if string representation needed).
Ex: seconds2str(86400*390) = 1y 3w 4d
seconds2str(86400*368) = 1y 3d
seconds2str(86400*365.5) = 1y
seconds2str(86400*2+3600*7+60*15) = 2d 7h 15m
seconds2str(86400*2+3599) = 2d 1h
seconds2str(3600-5) = 1h
seconds2str(3600-10) = 59m 50s
seconds2str(59) = 59s
"""
def __init__(self, sec):
self.sec = sec
def __str__(self):
# s = str(datetime.timedelta(seconds=int(self.sec)))
# return s if s[-3:] != ":00" else s[:-3]
s = self.sec; c = 3
# automatic accuracy: round by large values (and maximally 3 groups)
if s >= 31536000: # a year as 365*24*60*60 (don't need to consider leap year by this accuracy)
s = int(round(float(s)/86400)) # round by a day
r = str(s//365) + 'y '; s %= 365
if s >= 7:
r += str(s//7) + 'w '; s %= 7
if s:
r += str(s) + 'd '
return r[:-1]
if s >= 604800: # a week as 24*60*60*7
s = int(round(float(s)/3600)) # round by a hour
r = str(s//168) + 'w '; s %= 168
if s >= 24:
r += str(s//24) + 'd '; s %= 24
if s:
r += str(s) + 'h '
return r[:-1]
if s >= 86400: # a day as 24*60*60
s = int(round(float(s)/60)) # round by a minute
r = str(s//1440) + 'd '; s %= 1440
if s >= 60:
r += str(s//60) + 'h '; s %= 60
if s:
r += str(s) + 'm '
return r[:-1]
if s >= 3595: # a hour as 60*60 (- 5 seconds)
s = int(round(float(s)/10)) # round by 10 seconds
r = str(s//360) + 'h '; s %= 360
if s >= 6: # a minute
r += str(s//6) + 'm '; s %= 6
return r[:-1]
r = ''
if s >= 60: # a minute
r += str(s//60) + 'm '; s %= 60
if s: # remaining seconds
r += str(s) + 's '
elif not self.sec: # 0s
r = '0 '
return r[:-1]
def __repr__(self):
return self.__str__()

View File

@ -232,7 +232,7 @@ class ObserverThread(JailThread):
if self._paused: if self._paused:
continue continue
else: else:
## notify event deleted (shutdown) - just sleep a litle bit (waiting for shutdown events, prevent high cpu usage) ## notify event deleted (shutdown) - just sleep a little bit (waiting for shutdown events, prevent high cpu usage)
time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL) time.sleep(ObserverThread.DEFAULT_SLEEP_INTERVAL)
## stop by shutdown and empty queue : ## stop by shutdown and empty queue :
if not self.is_full: if not self.is_full:
@ -364,7 +364,7 @@ class ObserverThread(JailThread):
## [Async] ban time increment functionality ... ## [Async] ban time increment functionality ...
## ----------------------------------------- ## -----------------------------------------
def failureFound(self, failManager, jail, ticket): def failureFound(self, jail, ticket):
""" Notify observer a failure for ip was found """ Notify observer a failure for ip was found
Observer will check ip was known (bad) and possibly increase an retry count Observer will check ip was known (bad) and possibly increase an retry count
@ -372,7 +372,7 @@ class ObserverThread(JailThread):
# check jail active : # check jail active :
if not jail.isAlive() or not jail.getBanTimeExtra("increment"): if not jail.isAlive() or not jail.getBanTimeExtra("increment"):
return return
ip = ticket.getIP() ip = ticket.getID()
unixTime = ticket.getTime() unixTime = ticket.getTime()
logSys.debug("[%s] Observer: failure found %s", jail.name, ip) 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 ...) : # increase retry count for known (bad) ip, corresponding banCount of it (one try will count than 2, 3, 5, 9 ...) :
@ -380,7 +380,7 @@ class ObserverThread(JailThread):
retryCount = 1 retryCount = 1
timeOfBan = None timeOfBan = None
try: try:
maxRetry = failManager.getMaxRetry() maxRetry = jail.filter.failManager.getMaxRetry()
db = jail.database db = jail.database
if db is not None: if db is not None:
for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail): for banCount, timeOfBan, lastBanTime in db.getBan(ip, jail):
@ -403,18 +403,12 @@ class ObserverThread(JailThread):
MyTime.time2str(unixTime), banCount, retryCount, MyTime.time2str(unixTime), banCount, retryCount,
(', Ban' if retryCount >= maxRetry else '')) (', Ban' if retryCount >= maxRetry else ''))
# retryCount-1, because a ticket was already once incremented by filter self # retryCount-1, because a ticket was already once incremented by filter self
retryCount = failManager.addFailure(ticket, retryCount - 1, True) retryCount = jail.filter.failManager.addFailure(ticket, retryCount - 1, True)
ticket.setBanCount(banCount) ticket.setBanCount(banCount)
# after observe we have increased attempt count, compare it >= maxretry ... # after observe we have increased attempt count, compare it >= maxretry ...
if retryCount >= maxRetry: if retryCount >= maxRetry:
# perform the banning of the IP now (again) # perform the banning of the IP now (again)
# [todo]: this code part will be used multiple times - optimize it later. jail.filter.performBan(ip)
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: except Exception as e:
logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG) logSys.error('%s', e, exc_info=logSys.getEffectiveLevel()<=logging.DEBUG)
@ -441,7 +435,7 @@ class ObserverThread(JailThread):
if not jail.isAlive() or not jail.database: if not jail.isAlive() or not jail.database:
return banTime return banTime
be = jail.getBanTimeExtra() be = jail.getBanTimeExtra()
ip = ticket.getIP() ip = ticket.getID()
orgBanTime = banTime orgBanTime = banTime
# check ip was already banned (increment time of ban): # check ip was already banned (increment time of ban):
try: try:
@ -462,7 +456,7 @@ class ObserverThread(JailThread):
if ticket.getTime() > timeOfBan: if ticket.getTime() > timeOfBan:
logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount, logSys.info('[%s] IP %s is bad: %s # last %s - incr %s to %s' % (jail.name, ip, banCount,
MyTime.time2str(timeOfBan), MyTime.time2str(timeOfBan),
datetime.timedelta(seconds=int(orgBanTime)), datetime.timedelta(seconds=int(banTime)))); MyTime.seconds2str(orgBanTime), MyTime.seconds2str(banTime)))
else: else:
ticket.restored = True ticket.restored = True
break break
@ -480,7 +474,7 @@ class ObserverThread(JailThread):
return return
try: try:
oldbtime = btime oldbtime = btime
ip = ticket.getIP() ip = ticket.getID()
logSys.debug("[%s] Observer: ban found %s, %s", jail.name, ip, btime) 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 not permanent and ban time was not set - check time should be increased:
if btime != -1 and ticket.getBanTime() is None: if btime != -1 and ticket.getBanTime() is None:
@ -491,8 +485,7 @@ class ObserverThread(JailThread):
# if not permanent # if not permanent
if btime != -1: if btime != -1:
bendtime = ticket.getTime() + btime bendtime = ticket.getTime() + btime
logtime = (datetime.timedelta(seconds=int(btime)), logtime = (MyTime.seconds2str(btime), MyTime.time2str(bendtime))
MyTime.time2str(bendtime))
# check ban is not too old : # check ban is not too old :
if bendtime < MyTime.time(): if bendtime < MyTime.time():
logSys.debug('Ignore old bantime %s', logtime[1]) logSys.debug('Ignore old bantime %s', logtime[1])
@ -521,7 +514,7 @@ class ObserverThread(JailThread):
""" """
try: try:
btime = ticket.getBanTime() btime = ticket.getBanTime()
ip = ticket.getIP() ip = ticket.getID()
logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime) logSys.debug("[%s] Observer: prolong %s, %s", jail.name, ip, btime)
# prolong ticket via actions that expected this: # prolong ticket via actions that expected this:
jail.actions._prolongBan(ticket) jail.actions._prolongBan(ticket)

View File

@ -34,7 +34,7 @@ import sys
from .observer import Observers, ObserverThread from .observer import Observers, ObserverThread
from .jails import Jails from .jails import Jails
from .filter import FileFilter, JournalFilter from .filter import DNSUtils, FileFilter, JournalFilter
from .transmitter import Transmitter from .transmitter import Transmitter
from .asyncserver import AsyncServer, AsyncServerException from .asyncserver import AsyncServer, AsyncServerException
from .. import version from .. import version
@ -293,6 +293,11 @@ class Server:
for name in self.__jails.keys(): for name in self.__jails.keys():
self.delJail(name, stop=False, join=True) self.delJail(name, stop=False, join=True)
def clearCaches(self):
# we need to clear caches, to be able to recognize new IPs/families etc:
DNSUtils.CACHE_nameToIp.clear()
DNSUtils.CACHE_ipToName.clear()
def reloadJails(self, name, opts, begin): def reloadJails(self, name, opts, begin):
if begin: if begin:
# begin reload: # begin reload:
@ -314,6 +319,8 @@ class Server:
if "--restart" in opts: if "--restart" in opts:
self.stopJail(name) self.stopJail(name)
else: else:
# invalidate caches by reload
self.clearCaches()
# first unban all ips (will be not restored after (re)start): # first unban all ips (will be not restored after (re)start):
if "--unban" in opts: if "--unban" in opts:
self.setUnbanIP() self.setUnbanIP()
@ -384,7 +391,7 @@ class Server:
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
return filter_.getLogPaths() return filter_.getLogPaths()
else: # pragma: systemd no cover else: # pragma: systemd no cover
logSys.info("Jail %s is not a FileFilter instance" % name) logSys.debug("Jail %s is not a FileFilter instance" % name)
return [] return []
def addJournalMatch(self, name, match): # pragma: systemd no cover def addJournalMatch(self, name, match): # pragma: systemd no cover
@ -402,7 +409,7 @@ class Server:
if isinstance(filter_, JournalFilter): if isinstance(filter_, JournalFilter):
return filter_.getJournalMatch() return filter_.getJournalMatch()
else: else:
logSys.info("Jail %s is not a JournalFilter instance" % name) logSys.debug("Jail %s is not a JournalFilter instance" % name)
return [] return []
def setLogEncoding(self, name, encoding): def setLogEncoding(self, name, encoding):
@ -671,7 +678,10 @@ class Server:
return True return True
padding = logOptions.get('padding') padding = logOptions.get('padding')
# set a format which is simpler for console use # set a format which is simpler for console use
if systarget == "SYSLOG": if systarget == "SYSTEMD-JOURNAL":
from systemd.journal import JournalHandler
hdlr = JournalHandler(SYSLOG_IDENTIFIER='fail2ban')
elif systarget == "SYSLOG":
facility = logOptions.get('facility', 'DAEMON').upper() facility = logOptions.get('facility', 'DAEMON').upper()
# backwards compatibility - default no padding for syslog handler: # backwards compatibility - default no padding for syslog handler:
if padding is None: padding = '0' if padding is None: padding = '0'
@ -721,9 +731,7 @@ class Server:
except (ValueError, KeyError): # pragma: no cover except (ValueError, KeyError): # pragma: no cover
# Is known to be thrown after logging was shutdown once # Is known to be thrown after logging was shutdown once
# with older Pythons -- seems to be safe to ignore there # with older Pythons -- seems to be safe to ignore there
# At least it was still failing on 2.6.2-0ubuntu1 (jaunty) if sys.version_info < (3,) or sys.version_info >= (3, 2):
if (2, 6, 3) <= sys.version_info < (3,) or \
(3, 2) <= sys.version_info:
raise raise
# detailed format by deep log levels (as DEBUG=10): # detailed format by deep log levels (as DEBUG=10):
if logger.getEffectiveLevel() <= logging.DEBUG: # pragma: no cover if logger.getEffectiveLevel() <= logging.DEBUG: # pragma: no cover
@ -749,6 +757,7 @@ class Server:
verbose = self.__verbose-1 verbose = self.__verbose-1
fmt = getVerbosityFormat(verbose, addtime=addtime, padding=padding) fmt = getVerbosityFormat(verbose, addtime=addtime, padding=padding)
# tell the handler to use this format # tell the handler to use this format
if target != "SYSTEMD-JOURNAL":
hdlr.setFormatter(logging.Formatter(fmt)) hdlr.setFormatter(logging.Formatter(fmt))
logger.addHandler(hdlr) logger.addHandler(hdlr)
# Does not display this message at startup. # Does not display this message at startup.
@ -788,7 +797,7 @@ class Server:
return self.__syslogSocket return self.__syslogSocket
def flushLogs(self): def flushLogs(self):
if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG']: if self.__logTarget not in ['STDERR', 'STDOUT', 'SYSLOG', 'SYSTEMD-JOURNAL']:
for handler in getLogger("fail2ban").handlers: for handler in getLogger("fail2ban").handlers:
try: try:
handler.doRollover() handler.doRollover()
@ -803,6 +812,11 @@ class Server:
logSys.info("flush performed on %s" % self.__logTarget) logSys.info("flush performed on %s" % self.__logTarget)
return "flushed" return "flushed"
@staticmethod
def setIPv6IsAllowed(value):
value = _as_bool(value) if value != 'auto' else None
return DNSUtils.setIPv6IsAllowed(value)
def setThreadOptions(self, value): def setThreadOptions(self, value):
for o, v in value.iteritems(): for o, v in value.iteritems():
if o == 'stacksize': if o == 'stacksize':
@ -839,6 +853,26 @@ class Server:
def getDatabase(self): def getDatabase(self):
return self.__db return self.__db
@staticmethod
def __get_fdlist():
"""Generate a list of open file descriptors.
This wouldn't work on some platforms, or if proc/fdescfs not mounted, or a chroot environment,
then it'd raise a FileExistsError.
"""
for path in (
'/proc/self/fd', # Linux, Cygwin and NetBSD
'/proc/fd', # MacOS and FreeBSD
):
if os.path.exists(path):
def fdlist():
for name in os.listdir(path):
if name.isdigit():
yield int(name)
return fdlist()
# other platform or unmounted, chroot etc:
raise FileExistsError("fd-list not found")
def __createDaemon(self): # pragma: no cover def __createDaemon(self): # pragma: no cover
""" Detach a process from the controlling terminal and run it in the """ Detach a process from the controlling terminal and run it in the
background as a daemon. background as a daemon.
@ -896,25 +930,37 @@ class Server:
# Signal to exit, parent of the first child. # Signal to exit, parent of the first child.
return None return None
# Close all open files. Try the system configuration variable, SC_OPEN_MAX, # Close all open files. Try to obtain the range of open descriptors directly.
# As a fallback try the system configuration variable, SC_OPEN_MAX,
# for the maximum number of open files to close. If it doesn't exist, use # for the maximum number of open files to close. If it doesn't exist, use
# the default value (configurable). # the default value (configurable).
try:
fdlist = self.__get_fdlist()
maxfd = -1
except:
try: try:
maxfd = os.sysconf("SC_OPEN_MAX") maxfd = os.sysconf("SC_OPEN_MAX")
except (AttributeError, ValueError): except (AttributeError, ValueError):
maxfd = 256 # default maximum maxfd = 256 # default maximum
fdlist = xrange(maxfd+1)
# urandom should not be closed in Python 3.4.0. Fixed in 3.4.1 # urandom should not be closed in Python 3.4.0. Fixed in 3.4.1
# http://bugs.python.org/issue21207 # http://bugs.python.org/issue21207
if sys.version_info[0:3] == (3, 4, 0): # pragma: no cover if sys.version_info[0:3] == (3, 4, 0): # pragma: no cover
urandom_fd = os.open("/dev/urandom", os.O_RDONLY) urandom_fd = os.open("/dev/urandom", os.O_RDONLY)
for fd in range(0, maxfd): for fd in fdlist:
try: try:
if not os.path.sameopenfile(urandom_fd, fd): if not os.path.sameopenfile(urandom_fd, fd):
os.close(fd) os.close(fd)
except OSError: # ERROR (ignore) except OSError: # ERROR (ignore)
pass pass
os.close(urandom_fd) os.close(urandom_fd)
elif maxfd == -1:
for fd in fdlist:
try:
os.close(fd)
except OSError: # ERROR (ignore)
pass
else: else:
os.closerange(0, maxfd) os.closerange(0, maxfd)

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