MRG: from 0.9

pull/548/head
Daniel Black 2014-01-07 16:11:40 +11:00
commit 9e087b508d
74 changed files with 2354 additions and 1639 deletions

View File

@ -54,10 +54,12 @@ configuration before relying on it.
into logging messages in case of error or at DEBUG loglevel. into logging messages in case of error or at DEBUG loglevel.
* Added action xarf-login-attack to report formatted attack messages * Added action xarf-login-attack to report formatted attack messages
according to the XARF standard (v0.2). Close gh-105 according to the XARF standard (v0.2). Close gh-105
* Add filter for apache-modsecurity
* Support PyPy * Support PyPy
* Filter for stunnel
- Enhancements - Enhancements
* Jail names increased to 26 characters and iptables prefix reduced
from fail2ban- to f2b- as suggested by buanzo in gh-462.
* Multiline filter for sendmail-spam. Close gh-418 * Multiline filter for sendmail-spam. Close gh-418
* Multiline regex for Disconnecting: Too many authentication failures for * Multiline regex for Disconnecting: Too many authentication failures for
root [preauth]\nConnection closed by 6X.XXX.XXX.XXX [preauth] root [preauth]\nConnection closed by 6X.XXX.XXX.XXX [preauth]
@ -90,8 +92,11 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better
- Fix firewall-cmd actioncheck - patch from Adam Tkac. Redhat Bug #979622 - Fix firewall-cmd actioncheck - patch from Adam Tkac. Redhat Bug #979622
- Fix apache-common for apache-2.4 log file format. Thanks Mark White. - Fix apache-common for apache-2.4 log file format. Thanks Mark White.
Closes gh-516 Closes gh-516
- Asynchat changed to use push method which verifys whether all data was
send. This ensures that all data is sent before closing the connection.
- Enhancements: - Enhancements:
- added firewallcmd-ipset action
- long names on jails documented based on iptables limit of 30 less - long names on jails documented based on iptables limit of 30 less
len("fail2ban-"). len("fail2ban-").
- remove indentation of name and loglevel while logging to SYSLOG to - remove indentation of name and loglevel while logging to SYSLOG to
@ -100,14 +105,18 @@ ver. 0.8.12 (2013/12/XX) - things-can-only-get-better
- updated check_fail2ban to return performance data for all jails. - updated check_fail2ban to return performance data for all jails.
- filter apache-noscript now includes php cgi scripts. - filter apache-noscript now includes php cgi scripts.
Thanks dani. Closes gh-503 Thanks dani. Closes gh-503
- added ufw action. Thanks Guilhem Lettron. lp-#701522
- exim-spam filter to match spamassassin log entry for option SAdevnull.
Thanks Ivo Truxa. Closes gh-533
- filter.d/nsd.conf -- also amended Unix date template to match nsd format
- loglines now also report "[PID]" after the name portion
- New Features: - New Features:
* filter.d/solid-pop3d -- added thanks to Jacques Lav!gnotte on mailinglist. - Added filter for solid-pop3d -- thanks to Jacques Lav!gnotte on mailinglist.
* filter.d/nsd.conf -- also amended Unix date template to match nsd format - Added filter for apache-modsecurity
- Added filter for openwebmail thanks Ivo Truxa. Closes gh-543
- Enhancements: - Added filter for horde
- loglines now also report "[PID]" after the name portion
ver. 0.8.11 (2013/11/13) - loves-unittests-and-tight-DoS-free-filter-regexes ver. 0.8.11 (2013/11/13) - loves-unittests-and-tight-DoS-free-filter-regexes

460
DEVELOP
View File

@ -34,465 +34,7 @@ When submitting pull requests on GitHub we ask you to:
* Include a change to the relevant section of the ChangeLog; and * Include a change to the relevant section of the ChangeLog; and
* Include yourself in THANKS if not already there. * Include yourself in THANKS if not already there.
Filters If you are developing filters see the FILTERS file for documentation.
=======
Filters are tricky. They need to:
* work with a variety of the versions of the software that generates the logs;
* work with the range of logging configuration options available in the
software;
* work with multiple operating systems;
* not make assumptions about the log format in excess of the software
(e.g. do not assume a username doesn't contain spaces and use \S+ unless
you've checked the source code);
* account for how future versions of the software will log messages
(e.g. guess what would happen to the log message if different authentication
types are added);
* not be susceptible to DoS vulnerabilities (see Filter Security below); and
* match intended log lines only.
Please follow the steps from Filter Test Cases to Developing Filter Regular
Expressions and submit a GitHub pull request (PR) afterwards. If you get stuck,
you can push your unfinished changes and still submit a PR -- describe
what you have done, what is the hurdle, and we'll attempt to help (PR
will be automagically updated with future commits you would push to
complete it).
Filter test cases
-----------------
Purpose:
Start by finding the log messages that the application generates related to
some form of authentication failure. If you are adding to an existing filter
think about whether the log messages are of a similar importance and purpose
to the existing filter. If you were a user of Fail2Ban, and did a package
update of Fail2Ban that started matching new log messages, would anything
unexpected happen? Would the bantime/findtime for the jail be appropriate for
the new log messages? If it doesn't, perhaps it needs to be in a separate
filter definition, for example like exim filter aims at authentication failures
and exim-spam at log messages related to spam.
Even if it is a new filter you may consider separating the log messages into
different filters based on purpose.
Cause:
Are some of the log lines a result of the same action? For example, is a PAM
failure log message, followed by an application specific failure message the
result of the same user/script action? If you add regular expressions for
both you would end up with two failures for a single action.
Therefore, select the most appropriate log message and document the other log
message) with a test case not to match it and a description as to why you chose
one over another.
With the selected log lines consider what action has caused those log
messages and whether they could have been generated by accident? Could
the log message be occurring due to the first step towards the application
asking for authentication? Could the log messages occur often? If some of
these are true make a note of this in the jail.conf example that you provide.
Samples:
It is important to include log file samples so any future change in the regular
expression will still work with the log lines you have identified.
The sample log messages are provided in a file under testcases/files/logs/
named identically as the corresponding filter (but without .conf extension).
Each log line should be preceded by a line with failJSON metadata (so the logs
lines are tested in the test suite) directly above the log line. If there is
any specific information about the log message, such as version or an
application configuration option that is needed for the message to occur,
include this in a comment (line beginning with #) above the failJSON metadata.
Log samples should include only one, definitely not more than 3, examples of
log messages of the same form. If log messages are different in different
versions of the application log messages that show this are encouraged.
Also attempt to inject an IP into the application (e.g. by specifying
it as a username) so that Fail2Ban possibly detects the IP
from user input rather than the true origin. See the Filter Security section
and the top example in testcases/files/logs/apache-auth as to how to do this.
One you have discovered that this is possible, correct the regex so it doesn't
match and provide this as a test case with "match": false (see failJSON below).
If the mechanism to create the log message isn't obvious provide a
configuration and/or sample scripts testcases/files/config/{filtername} and
reference these in the comments above the log line.
FailJSON metadata:
A failJSON metadata is a comment immediately above the log message. It will
look like:
# failJSON: { "time": "2013-06-10T10:10:59", "match": true , "host": "93.184.216.119" }
Time should match the time of the log message. It is in a specific format of
Year-Month-Day'T'Hour:minute:Second. If your log message does not include a
year, like the example below, the year should be listed as 2005, if before Sun
Aug 14 10am UTC, and 2004 if afterwards. Here is an example failJSON
line preceding a sample log line:
# failJSON: { "time": "2005-03-24T15:25:51", "match": true , "host": "198.51.100.87" }
Mar 24 15:25:51 buffalo1 dropbear[4092]: bad password attempt for 'root' from 198.51.100.87:5543
The "host" in failJSON should contain the IP or domain that should be blocked.
For long lines that you do not want to be matched (e.g. from log injection
attacks) and any log lines to be excluded (see "Cause" section above), set
"match": false in the failJSON and describe the reason in the comment above.
After developing regexes, the following command will test all failJSON metadata
against the log lines in all sample log files
./fail2ban-testcases testSampleRegex
Developing Filter Regular Expressions
-------------------------------------
Date/Time:
At the moment, Fail2Ban depends on log lines to have time stamps. That is why
before starting to develop failregex, check if your log line format known to
Fail2Ban. Copy the time component from the log line and append an IP address to
test with following command:
./fail2ban-regex "2013-09-19 02:46:12 1.2.3.4" "<HOST>"
Output of such command should contain something like:
Date template hits:
|- [# of hits] date format
| [1] Year-Month-Day Hour:Minute:Second
Ensure that the template description matches time/date elements in your log line
time stamp. If there is no matched format then date template needs to be added
to server/datedetector.py. Ensure that a new template is added in the order
that more specific matches occur first and that there is no confusion between a
Day and a Month.
Filter file:
The filter is specified in a config/filter.d/{filtername}.conf file. Filter file
can have sections INCLUDES (optional) and Definition as follows:
[INCLUDES]
before = common.conf
after = filtername.local
[Definition]
failregex = ....
ignoreregex = ....
This is also documented in the man page jail.conf (section 5). Other definitions
can be added to make failregex's more readable and maintainable to be used
through string Interpolations (see http://docs.python.org/2.7/library/configparser.html)
General rules:
Use "before" if you need to include a common set of rules, like syslog or if
there is a common set of regexes for multiple filters.
Use "after" if you wish to allow the user to overwrite a set of customisations
of the current filter. This file doesn't need to exist.
Try to avoid using ignoreregex mainly for performance reasons. The case when you
would use it is if in trying to avoid using it, you end up with an unreadable
failregex.
Syslog:
If your application logs to syslog you can take advantage of log line prefix
definitions present in common.conf. So as a base use:
[INCLUDES]
before = common.conf
[Definition]
_daemon = app
failregex = ^%(__prefix_line)s
In this example common.conf defines __prefix_line which also contains the
_daemon name (in syslog terms the service) you have just specified. _daemon
can also be a regex.
For example, to capture following line _daemon should be set to "dovecot"
Dec 12 11:19:11 dunnart dovecot: pop3-login: Aborted login (tried to use disabled plaintext auth): rip=190.210.136.21, lip=113.212.99.193
and then ^%(__prefix_line)s would match "Dec 12 11:19:11 dunnart dovecot:
". Note it matches the trailing space(s) as well.
Substitutions (AKA string interpolations):
We have used string interpolations in above examples. They are useful for
making the regexes more readable, reuse generic patterns in multiple failregex
lines, and also to refer definition of regex parts to specific filters or even
to the user. General principle is that value of a _name variable replaces
occurrences of %(_name)s within the same section or anywhere in the config file
if defined in [DEFAULT] section.
Regular Expressions:
Regular expressions (failregex, ignoreregex) assume that the date/time has been
removed from the log line (this is just how fail2ban works internally ATM).
If the format is like '<date...> error 1.2.3.4 is evil' then you need to match
the < at the start so regex should be similar to '^<> <HOST> is evil$' using
<HOST> where the IP/domain name appears in the log line.
The following general rules apply to regular expressions:
* ensure regexes start with a ^ and are as restrictive as possible. E.g. do not
use .* if \d+ is sufficient;
* use functionality of Python regexes defined in the standard Python re library
http://docs.python.org/2/library/re.html;
* make regular expressions readable (as much as possible). E.g.
(?:...) represents a non-capturing regex but (...) is more readable, thus
preferred.
If you have only a basic knowledge of regular repressions we advise to read
http://docs.python.org/2/library/re.html first. It doesn't take long and would
remind you e.g. which characters you need to escape and which you don't.
Developing/testing a regex:
You can develop a regex in a file or using command line depending on your
preference. You can also use samples you have already created in the test cases
or test them one at a time.
The general tool for testing Fail2Ban regexes is fail2ban-regex. To see how to
use it run:
./fail2ban-regex --help
Take note of -l heavydebug / -l debug and -v as they might be very useful.
TIP: Take a look at the source code of the application you are developing
failregex for. You may see optional or extra log messages, or parts there
of, that need to form part of your regex. It may also reveal how some
parts are constrained and different formats depending on configuration or
less common usages.
TIP: For looking through source code - http://sourcecodebrowser.com/ . It has
call graphs and can browse different versions.
TIP: Some applications log spaces at the end. If you are not sure add \s*$ as
the end part of the regex.
If your regex is not matching, http://www.debuggex.com/?flavor=python can help
to tune it. fail2ban-regex -D ... will present Debuggex URLs for the regexs
and sample log files that you pass into it.
In general use when using regex debuggers for generating fail2ban filters:
* use regex from the ./fail2ban-regex output (to ensure all substitutions are
done)
* replace <HOST> with (?&.ipv4)
* make sure that regex type set to Python
* for the test data put your log output with the date/time removed
When you have fixed the regex put it back into your filter file.
Please spread the good word about Debuggex - Serge Toarca is kindly continuing
its free availability to Open Source developers.
Finishing up:
If you've added a new filter, add a new entry in config/jail.conf. The theory
here is that a user will create a jail.local with [filtername]\nenable=true to
enable your jail.
So more specifically in the [filter] section in jail.conf:
* ensure that you have "enabled = false" (users will enable as needed);
* use "filter =" set to your filter name;
* use a typical action to disable ports associated with the application;
* set "logpath" to the usual location of application log file;
* if the default findtime or bantime isn't appropriate to the filter, specify
more appropriate choices (possibly with a brief comment line).
Submit github pull request (See "Pull Requests" above) for
github.com/fail2ban/fail2ban containing your great work.
Filter Security
---------------
Poor filter regular expressions are susceptible to DoS attacks.
When a remote user has the ability to introduce text that would match filter's
failregex, while matching inserted text to the <HOST> part, they have the
ability to deny any host they choose.
So the <HOST> part must be anchored on text generated by the application, and
not the user, to an extent sufficient to prevent user inserting the entire text
matching this or any other failregex.
Ideally filter regex should anchor at the beginning and at the end of log line.
However as more applications log at the beginning than the end, anchoring the
beginning is more important. If the log file used by the application is shared
with other applications, like system logs, ensure the other application that use
that log file do not log user generated text at the beginning of the line, or,
if they do, ensure the regexes of the filter are sufficient to mitigate the risk
of insertion.
Examples of poor filters
------------------------
1. Too restrictive
We find a log message:
Apr-07-13 07:08:36 Invalid command fial2ban from 1.2.3.4
We make a failregex
^Invalid command \S+ from <HOST>
Now think evil. The user does the command 'blah from 1.2.3.44'
The program diligently logs:
Apr-07-13 07:08:36 Invalid command blah from 1.2.3.44 from 1.2.3.4
And fail2ban matches 1.2.3.44 as the IP that it ban. A DoS attack was successful.
The fix here is that the command can be anything so .* is appropriate.
^Invalid command .* from <HOST>
Here the .* will match until the end of the string. Then realise it has more to
match, i.e. "from <HOST>" and go back until it find this. Then it will ban
1.2.3.4 correctly. Since the <HOST> is always at the end, end the regex with a $.
^Invalid command .* from <HOST>$
Note if we'd just had the expression:
^Invalid command \S+ from <HOST>$
Then provided the user put a space in their command they would have never been
banned.
2. Unanchored regex can match other user injected data
From the Apache vulnerability CVE-2013-2178
( original ref: https://vndh.net/note:fail2ban-089-denial-service ).
An example bad regex for Apache:
failregex = [[]client <HOST>[]] user .* not found
Since the user can do a get request on:
GET /[client%20192.168.0.1]%20user%20root%20not%20found HTTP/1.0
Host: remote.site
Now the log line will be:
[Sat Jun 01 02:17:42 2013] [error] [client 192.168.33.1] File does not exist: /srv/http/site/[client 192.168.0.1] user root not found
As this log line doesn't match other expressions hence it matches the above
regex and blocks 192.168.33.1 as a denial of service from the HTTP requester.
3. Over greedy pattern matching
From: https://github.com/fail2ban/fail2ban/pull/426
An example ssh log (simplified)
Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser remoteuser
As we assume username can include anything including spaces its prudent to put
.* here. The remote user can also exist as anything so lets not make assumptions again.
failregex = ^%(__prefix_line)sFailed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
So this works. The problem is if the .* after remote user is injected by the
user to be 'from 1.2.3.4'. The resultant log line is.
Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4
Testing with:
fail2ban-regex -v 'Sep 29 17:15:02 Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4' '^ Failed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$'
TIP: I've removed the bit that matches __prefix_line from the regex and log.
Shows:
1) [1] ^ Failed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
1.2.3.4 Sun Sep 29 17:15:02 2013
It should of matched 127.0.0.1. So the first greedy part of the greedy regex
matched until the end of the string. The was no "from <HOST>" so the regex
engine worked backwards from the end of the string until this was matched.
The result was that 1.2.3.4 was matched, injected by the user, and the wrong IP
was banned.
The solution here is to make the first .* non-greedy with .*?. Here it matches
as little as required and the fail2ban-regex tool shows the output:
fail2ban-regex -v 'Sep 29 17:15:02 Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4' '^ Failed \S+ for .*? from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$'
1) [1] ^ Failed \S+ for .*? from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
127.0.0.1 Sun Sep 29 17:15:02 2013
So the general case here is a log line that contains:
(fixed_data_1)<HOST>(fixed_data_2)(user_injectable_data)
Where the regex that matches fixed_data_1 is gready and matches the entire
string, before moving backwards and user_injectable_data can match the entire
string.
Another case:
ref: https://www.debuggex.com/r/CtAbeKMa2sDBEfA2/0
A webserver logs the following without URL escaping:
[error] 2865#0: *66647 user "xyz" was not found in "/file", client: 1.2.3.1, server: www.host.com, request: "GET ", client: 3.2.1.1, server: fake.com, request: "GET exploited HTTP/3.3", host: "injected.host", host: "www.myhost.com"
regex:
failregex = ^ \[error\] \d+#\d+: \*\d+ user "\S+":? (?:password mismatch|was not found in ".*"), client: <HOST>, server: \S+, request: "\S+ .+ HTTP/\d+\.\d+", host: "\S+"
The .* matches to the end of the string. Finds that it can't continue to match
", client ... so it moves from the back and find that the user injected web URL:
", client: 3.2.1.1, server: fake.com, request: "GET exploited HTTP/3.3", host: "injected.host
In this case there is a fixed host: "www.myhost.com" at the end so the solution
is to anchor the regex at the end with a $.
If this wasn't the case then first .* needed to be made so it didn't capture
beyond <HOST>.
4. Application generates two identical log messages with different meanings
If the application generates the following two messages under different
circumstances:
client <IP>: authentication failed
client <USER>: authentication failed
Then it's obvious that a regex of "^client <HOST>: authentication
failed$" will still cause problems if the user can trigger the second
log message with a <USER> of 123.1.1.1.
Here there's nothing to do except request/change the application so it logs
messages differently.
Code Testing Code Testing
============ ============

469
FILTERS Normal file
View File

@ -0,0 +1,469 @@
__ _ _ ___ _
/ _|__ _(_) |_ ) |__ __ _ _ _
| _/ _` | | |/ /| '_ \/ _` | ' \
|_| \__,_|_|_/___|_.__/\__,_|_||_|
================================================================================
Developing Filters
================================================================================
Filters
=======
Filters are tricky. They need to:
* work with a variety of the versions of the software that generates the logs;
* work with the range of logging configuration options available in the
software;
* work with multiple operating systems;
* not make assumptions about the log format in excess of the software
(e.g. do not assume a username doesn't contain spaces and use \S+ unless
you've checked the source code);
* account for how future versions of the software will log messages
(e.g. guess what would happen to the log message if different authentication
types are added);
* not be susceptible to DoS vulnerabilities (see Filter Security below); and
* match intended log lines only.
Please follow the steps from Filter Test Cases to Developing Filter Regular
Expressions and submit a GitHub pull request (PR) afterwards. If you get stuck,
you can push your unfinished changes and still submit a PR -- describe
what you have done, what is the hurdle, and we'll attempt to help (PR
will be automagically updated with future commits you would push to
complete it).
Filter test cases
-----------------
Purpose:
Start by finding the log messages that the application generates related to
some form of authentication failure. If you are adding to an existing filter
think about whether the log messages are of a similar importance and purpose
to the existing filter. If you were a user of Fail2Ban, and did a package
update of Fail2Ban that started matching new log messages, would anything
unexpected happen? Would the bantime/findtime for the jail be appropriate for
the new log messages? If it doesn't, perhaps it needs to be in a separate
filter definition, for example like exim filter aims at authentication failures
and exim-spam at log messages related to spam.
Even if it is a new filter you may consider separating the log messages into
different filters based on purpose.
Cause:
Are some of the log lines a result of the same action? For example, is a PAM
failure log message, followed by an application specific failure message the
result of the same user/script action? If you add regular expressions for
both you would end up with two failures for a single action.
Therefore, select the most appropriate log message and document the other log
message) with a test case not to match it and a description as to why you chose
one over another.
With the selected log lines consider what action has caused those log
messages and whether they could have been generated by accident? Could
the log message be occurring due to the first step towards the application
asking for authentication? Could the log messages occur often? If some of
these are true make a note of this in the jail.conf example that you provide.
Samples:
It is important to include log file samples so any future change in the regular
expression will still work with the log lines you have identified.
The sample log messages are provided in a file under testcases/files/logs/
named identically as the corresponding filter (but without .conf extension).
Each log line should be preceded by a line with failJSON metadata (so the logs
lines are tested in the test suite) directly above the log line. If there is
any specific information about the log message, such as version or an
application configuration option that is needed for the message to occur,
include this in a comment (line beginning with #) above the failJSON metadata.
Log samples should include only one, definitely not more than 3, examples of
log messages of the same form. If log messages are different in different
versions of the application log messages that show this are encouraged.
Also attempt to inject an IP into the application (e.g. by specifying
it as a username) so that Fail2Ban possibly detects the IP
from user input rather than the true origin. See the Filter Security section
and the top example in testcases/files/logs/apache-auth as to how to do this.
One you have discovered that this is possible, correct the regex so it doesn't
match and provide this as a test case with "match": false (see failJSON below).
If the mechanism to create the log message isn't obvious provide a
configuration and/or sample scripts testcases/files/config/{filtername} and
reference these in the comments above the log line.
FailJSON metadata:
A failJSON metadata is a comment immediately above the log message. It will
look like:
# failJSON: { "time": "2013-06-10T10:10:59", "match": true , "host": "93.184.216.119" }
Time should match the time of the log message. It is in a specific format of
Year-Month-Day'T'Hour:minute:Second. If your log message does not include a
year, like the example below, the year should be listed as 2005, if before Sun
Aug 14 10am UTC, and 2004 if afterwards. Here is an example failJSON
line preceding a sample log line:
# failJSON: { "time": "2005-03-24T15:25:51", "match": true , "host": "198.51.100.87" }
Mar 24 15:25:51 buffalo1 dropbear[4092]: bad password attempt for 'root' from 198.51.100.87:5543
The "host" in failJSON should contain the IP or domain that should be blocked.
For long lines that you do not want to be matched (e.g. from log injection
attacks) and any log lines to be excluded (see "Cause" section above), set
"match": false in the failJSON and describe the reason in the comment above.
After developing regexes, the following command will test all failJSON metadata
against the log lines in all sample log files
./fail2ban-testcases testSampleRegex
Developing Filter Regular Expressions
-------------------------------------
Date/Time:
At the moment, Fail2Ban depends on log lines to have time stamps. That is why
before starting to develop failregex, check if your log line format known to
Fail2Ban. Copy the time component from the log line and append an IP address to
test with following command:
./fail2ban-regex "2013-09-19 02:46:12 1.2.3.4" "<HOST>"
Output of such command should contain something like:
Date template hits:
|- [# of hits] date format
| [1] Year-Month-Day Hour:Minute:Second
Ensure that the template description matches time/date elements in your log line
time stamp. If there is no matched format then date template needs to be added
to server/datedetector.py. Ensure that a new template is added in the order
that more specific matches occur first and that there is no confusion between a
Day and a Month.
Filter file:
The filter is specified in a config/filter.d/{filtername}.conf file. Filter file
can have sections INCLUDES (optional) and Definition as follows:
[INCLUDES]
before = common.conf
after = filtername.local
[Definition]
failregex = ....
ignoreregex = ....
This is also documented in the man page jail.conf (section 5). Other definitions
can be added to make failregex's more readable and maintainable to be used
through string Interpolations (see http://docs.python.org/2.7/library/configparser.html)
General rules:
Use "before" if you need to include a common set of rules, like syslog or if
there is a common set of regexes for multiple filters.
Use "after" if you wish to allow the user to overwrite a set of customisations
of the current filter. This file doesn't need to exist.
Try to avoid using ignoreregex mainly for performance reasons. The case when you
would use it is if in trying to avoid using it, you end up with an unreadable
failregex.
Syslog:
If your application logs to syslog you can take advantage of log line prefix
definitions present in common.conf. So as a base use:
[INCLUDES]
before = common.conf
[Definition]
_daemon = app
failregex = ^%(__prefix_line)s
In this example common.conf defines __prefix_line which also contains the
_daemon name (in syslog terms the service) you have just specified. _daemon
can also be a regex.
For example, to capture following line _daemon should be set to "dovecot"
Dec 12 11:19:11 dunnart dovecot: pop3-login: Aborted login (tried to use disabled plaintext auth): rip=190.210.136.21, lip=113.212.99.193
and then ^%(__prefix_line)s would match "Dec 12 11:19:11 dunnart dovecot:
". Note it matches the trailing space(s) as well.
Substitutions (AKA string interpolations):
We have used string interpolations in above examples. They are useful for
making the regexes more readable, reuse generic patterns in multiple failregex
lines, and also to refer definition of regex parts to specific filters or even
to the user. General principle is that value of a _name variable replaces
occurrences of %(_name)s within the same section or anywhere in the config file
if defined in [DEFAULT] section.
Regular Expressions:
Regular expressions (failregex, ignoreregex) assume that the date/time has been
removed from the log line (this is just how fail2ban works internally ATM).
If the format is like '<date...> error 1.2.3.4 is evil' then you need to match
the < at the start so regex should be similar to '^<> <HOST> is evil$' using
<HOST> where the IP/domain name appears in the log line.
The following general rules apply to regular expressions:
* ensure regexes start with a ^ and are as restrictive as possible. E.g. do not
use .* if \d+ is sufficient;
* use functionality of Python regexes defined in the standard Python re library
http://docs.python.org/2/library/re.html;
* make regular expressions readable (as much as possible). E.g.
(?:...) represents a non-capturing regex but (...) is more readable, thus
preferred.
If you have only a basic knowledge of regular repressions we advise to read
http://docs.python.org/2/library/re.html first. It doesn't take long and would
remind you e.g. which characters you need to escape and which you don't.
Developing/testing a regex:
You can develop a regex in a file or using command line depending on your
preference. You can also use samples you have already created in the test cases
or test them one at a time.
The general tool for testing Fail2Ban regexes is fail2ban-regex. To see how to
use it run:
./fail2ban-regex --help
Take note of -l heavydebug / -l debug and -v as they might be very useful.
TIP: Take a look at the source code of the application you are developing
failregex for. You may see optional or extra log messages, or parts there
of, that need to form part of your regex. It may also reveal how some
parts are constrained and different formats depending on configuration or
less common usages.
TIP: For looking through source code - http://sourcecodebrowser.com/ . It has
call graphs and can browse different versions.
TIP: Some applications log spaces at the end. If you are not sure add \s*$ as
the end part of the regex.
If your regex is not matching, http://www.debuggex.com/?flavor=python can help
to tune it. fail2ban-regex -D ... will present Debuggex URLs for the regexs
and sample log files that you pass into it.
In general use when using regex debuggers for generating fail2ban filters:
* use regex from the ./fail2ban-regex output (to ensure all substitutions are
done)
* replace <HOST> with (?&.ipv4)
* make sure that regex type set to Python
* for the test data put your log output with the date/time removed
When you have fixed the regex put it back into your filter file.
Please spread the good word about Debuggex - Serge Toarca is kindly continuing
its free availability to Open Source developers.
Finishing up:
If you've added a new filter, add a new entry in config/jail.conf. The theory
here is that a user will create a jail.local with [filtername]\nenable=true to
enable your jail.
So more specifically in the [filter] section in jail.conf:
* ensure that you have "enabled = false" (users will enable as needed);
* use "filter =" set to your filter name;
* use a typical action to disable ports associated with the application;
* set "logpath" to the usual location of application log file;
* if the default findtime or bantime isn't appropriate to the filter, specify
more appropriate choices (possibly with a brief comment line).
Submit github pull request (See "Pull Requests" above) for
github.com/fail2ban/fail2ban containing your great work.
Filter Security
---------------
Poor filter regular expressions are susceptible to DoS attacks.
When a remote user has the ability to introduce text that would match filter's
failregex, while matching inserted text to the <HOST> part, they have the
ability to deny any host they choose.
So the <HOST> part must be anchored on text generated by the application, and
not the user, to an extent sufficient to prevent user inserting the entire text
matching this or any other failregex.
Ideally filter regex should anchor at the beginning and at the end of log line.
However as more applications log at the beginning than the end, anchoring the
beginning is more important. If the log file used by the application is shared
with other applications, like system logs, ensure the other application that use
that log file do not log user generated text at the beginning of the line, or,
if they do, ensure the regexes of the filter are sufficient to mitigate the risk
of insertion.
Examples of poor filters
------------------------
1. Too restrictive
We find a log message:
Apr-07-13 07:08:36 Invalid command fial2ban from 1.2.3.4
We make a failregex
^Invalid command \S+ from <HOST>
Now think evil. The user does the command 'blah from 1.2.3.44'
The program diligently logs:
Apr-07-13 07:08:36 Invalid command blah from 1.2.3.44 from 1.2.3.4
And fail2ban matches 1.2.3.44 as the IP that it ban. A DoS attack was successful.
The fix here is that the command can be anything so .* is appropriate.
^Invalid command .* from <HOST>
Here the .* will match until the end of the string. Then realise it has more to
match, i.e. "from <HOST>" and go back until it find this. Then it will ban
1.2.3.4 correctly. Since the <HOST> is always at the end, end the regex with a $.
^Invalid command .* from <HOST>$
Note if we'd just had the expression:
^Invalid command \S+ from <HOST>$
Then provided the user put a space in their command they would have never been
banned.
2. Unanchored regex can match other user injected data
From the Apache vulnerability CVE-2013-2178
( original ref: https://vndh.net/note:fail2ban-089-denial-service ).
An example bad regex for Apache:
failregex = [[]client <HOST>[]] user .* not found
Since the user can do a get request on:
GET /[client%20192.168.0.1]%20user%20root%20not%20found HTTP/1.0
Host: remote.site
Now the log line will be:
[Sat Jun 01 02:17:42 2013] [error] [client 192.168.33.1] File does not exist: /srv/http/site/[client 192.168.0.1] user root not found
As this log line doesn't match other expressions hence it matches the above
regex and blocks 192.168.33.1 as a denial of service from the HTTP requester.
3. Over greedy pattern matching
From: https://github.com/fail2ban/fail2ban/pull/426
An example ssh log (simplified)
Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser remoteuser
As we assume username can include anything including spaces its prudent to put
.* here. The remote user can also exist as anything so lets not make assumptions again.
failregex = ^%(__prefix_line)sFailed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
So this works. The problem is if the .* after remote user is injected by the
user to be 'from 1.2.3.4'. The resultant log line is.
Sep 29 17:15:02 spaceman sshd[12946]: Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4
Testing with:
fail2ban-regex -v 'Sep 29 17:15:02 Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4' '^ Failed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$'
TIP: I've removed the bit that matches __prefix_line from the regex and log.
Shows:
1) [1] ^ Failed \S+ for .* from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
1.2.3.4 Sun Sep 29 17:15:02 2013
It should of matched 127.0.0.1. So the first greedy part of the greedy regex
matched until the end of the string. The was no "from <HOST>" so the regex
engine worked backwards from the end of the string until this was matched.
The result was that 1.2.3.4 was matched, injected by the user, and the wrong IP
was banned.
The solution here is to make the first .* non-greedy with .*?. Here it matches
as little as required and the fail2ban-regex tool shows the output:
fail2ban-regex -v 'Sep 29 17:15:02 Failed password for user from 127.0.0.1 port 20000 ssh1: ruser from 1.2.3.4' '^ Failed \S+ for .*? from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$'
1) [1] ^ Failed \S+ for .*? from <HOST>( port \d*)?( ssh\d+)?(: ruser .*)?$
127.0.0.1 Sun Sep 29 17:15:02 2013
So the general case here is a log line that contains:
(fixed_data_1)<HOST>(fixed_data_2)(user_injectable_data)
Where the regex that matches fixed_data_1 is gready and matches the entire
string, before moving backwards and user_injectable_data can match the entire
string.
Another case:
ref: https://www.debuggex.com/r/CtAbeKMa2sDBEfA2/0
A webserver logs the following without URL escaping:
[error] 2865#0: *66647 user "xyz" was not found in "/file", client: 1.2.3.1, server: www.host.com, request: "GET ", client: 3.2.1.1, server: fake.com, request: "GET exploited HTTP/3.3", host: "injected.host", host: "www.myhost.com"
regex:
failregex = ^ \[error\] \d+#\d+: \*\d+ user "\S+":? (?:password mismatch|was not found in ".*"), client: <HOST>, server: \S+, request: "\S+ .+ HTTP/\d+\.\d+", host: "\S+"
The .* matches to the end of the string. Finds that it can't continue to match
", client ... so it moves from the back and find that the user injected web URL:
", client: 3.2.1.1, server: fake.com, request: "GET exploited HTTP/3.3", host: "injected.host
In this case there is a fixed host: "www.myhost.com" at the end so the solution
is to anchor the regex at the end with a $.
If this wasn't the case then first .* needed to be made so it didn't capture
beyond <HOST>.
4. Application generates two identical log messages with different meanings
If the application generates the following two messages under different
circumstances:
client <IP>: authentication failed
client <USER>: authentication failed
Then it's obvious that a regex of "^client <HOST>: authentication
failed$" will still cause problems if the user can trigger the second
log message with a <USER> of 123.1.1.1.
Here there's nothing to do except request/change the application so it logs
messages differently.

View File

@ -5,6 +5,7 @@ TODO
THANKS THANKS
COPYING COPYING
DEVELOP DEVELOP
FILTERS
fail2ban-2to3 fail2ban-2to3
fail2ban-testcases-all fail2ban-testcases-all
fail2ban-testcases-all-python3 fail2ban-testcases-all-python3
@ -25,6 +26,7 @@ fail2ban/client/__init__.py
fail2ban/client/configurator.py fail2ban/client/configurator.py
fail2ban/client/csocket.py fail2ban/client/csocket.py
fail2ban/server/asyncserver.py fail2ban/server/asyncserver.py
fail2ban/server/database.py
fail2ban/server/filter.py fail2ban/server/filter.py
fail2ban/server/filterpyinotify.py fail2ban/server/filterpyinotify.py
fail2ban/server/filtergamin.py fail2ban/server/filtergamin.py
@ -62,6 +64,10 @@ fail2ban/tests/sockettestcase.py
fail2ban/tests/utils.py fail2ban/tests/utils.py
fail2ban/tests/misctestcase.py fail2ban/tests/misctestcase.py
fail2ban/tests/databasetestcase.py fail2ban/tests/databasetestcase.py
fail2ban/tests/config/jail.conf
fail2ban/tests/config/fail2ban.conf
fail2ban/tests/config/filter.d/simple.conf
fail2ban/tests/config/action.d/brokenaction.conf
fail2ban/tests/files/config/apache-auth/digest/.htaccess fail2ban/tests/files/config/apache-auth/digest/.htaccess
fail2ban/tests/files/config/apache-auth/digest/.htpasswd fail2ban/tests/files/config/apache-auth/digest/.htpasswd
fail2ban/tests/files/config/apache-auth/digest_time/.htaccess fail2ban/tests/files/config/apache-auth/digest_time/.htaccess
@ -131,6 +137,7 @@ fail2ban/tests/files/logs/selinux-ssh
fail2ban/tests/files/logs/sendmail-spam fail2ban/tests/files/logs/sendmail-spam
fail2ban/tests/files/logs/sieve fail2ban/tests/files/logs/sieve
fail2ban/tests/files/logs/squid fail2ban/tests/files/logs/squid
fail2ban/tests/files/logs/stunnel
fail2ban/tests/files/logs/suhosin fail2ban/tests/files/logs/suhosin
fail2ban/tests/files/logs/sogo-auth fail2ban/tests/files/logs/sogo-auth
fail2ban/tests/files/logs/solid-pop3d fail2ban/tests/files/logs/solid-pop3d
@ -156,6 +163,7 @@ setup.py
setup.cfg setup.cfg
kill-server kill-server
config/jail.conf config/jail.conf
config/fail2ban.conf
config/filter.d/common.conf config/filter.d/common.conf
config/filter.d/apache-auth.conf config/filter.d/apache-auth.conf
config/filter.d/apache-badbots.conf config/filter.d/apache-badbots.conf
@ -170,17 +178,22 @@ config/filter.d/exim.conf
config/filter.d/gssftpd.conf config/filter.d/gssftpd.conf
config/filter.d/suhosin.conf config/filter.d/suhosin.conf
config/filter.d/named-refused.conf config/filter.d/named-refused.conf
config/filter.d/openwebmail.conf
config/filter.d/pam-generic.conf
config/filter.d/php-url-fopen.conf
config/filter.d/postfix-sasl.conf
config/filter.d/pam-generic.conf
config/filter.d/php-url-fopen.conf
config/filter.d/postfix-sasl.conf
config/filter.d/postfix.conf config/filter.d/postfix.conf
config/filter.d/proftpd.conf config/filter.d/proftpd.conf
config/filter.d/pure-ftpd.conf config/filter.d/pure-ftpd.conf
config/filter.d/qmail.conf config/filter.d/qmail.conf
config/filter.d/pam-generic.conf
config/filter.d/php-url-fopen.conf
config/filter.d/postfix-sasl.conf
config/filter.d/sieve.conf config/filter.d/sieve.conf
config/filter.d/solid-pop3d.conf config/filter.d/solid-pop3d.conf
config/filter.d/sshd.conf config/filter.d/sshd.conf
config/filter.d/sshd-ddos.conf config/filter.d/sshd-ddos.conf
config/filter.d/stunnel.conf
config/filter.d/vsftpd.conf config/filter.d/vsftpd.conf
config/filter.d/webmin-auth.conf config/filter.d/webmin-auth.conf
config/filter.d/wuftpd.conf config/filter.d/wuftpd.conf
@ -213,7 +226,8 @@ config/action.d/osx-ipfw.conf
config/action.d/sendmail-common.conf config/action.d/sendmail-common.conf
config/action.d/bsd-ipfw.conf config/action.d/bsd-ipfw.conf
config/action.d/dummy.conf config/action.d/dummy.conf
config/action.d/firewall-cmd-direct-new.conf config/action.d/firewallcmd-new.conf
config/action.d/firewallcmd-ipset.conf
config/action.d/iptables-ipset-proto6-allports.conf config/action.d/iptables-ipset-proto6-allports.conf
config/action.d/iptables-blocktype.conf config/action.d/iptables-blocktype.conf
config/action.d/iptables-ipset-proto4.conf config/action.d/iptables-ipset-proto4.conf
@ -242,7 +256,8 @@ config/action.d/sendmail-whois-ipmatches.conf
config/action.d/sendmail-whois.conf config/action.d/sendmail-whois.conf
config/action.d/sendmail-whois-lines.conf config/action.d/sendmail-whois-lines.conf
config/action.d/shorewall.conf config/action.d/shorewall.conf
config/fail2ban.conf config/action.d/xarf-login-attack.conf
config/action.d/ufw.conf
doc/run-rootless.txt doc/run-rootless.txt
man/fail2ban-client.1 man/fail2ban-client.1
man/fail2ban.1 man/fail2ban.1

2
THANKS
View File

@ -35,9 +35,11 @@ ftoppi
François Boulogne François Boulogne
Frédéric Frédéric
Georgiy Mernov Georgiy Mernov
Guilhem Lettron
Guillaume Delvit Guillaume Delvit
Hanno 'Rince' Wagner Hanno 'Rince' Wagner
Iain Lea Iain Lea
Ivo Truxa
John Thoe John Thoe
Jacques Lav!gnotte Jacques Lav!gnotte
Ioan Indreias Ioan Indreias

View File

@ -41,7 +41,7 @@ except ImportError:
journal = None journal = None
from fail2ban.version import version from fail2ban.version import version
from fail2ban.client.configparserinc import SafeConfigParserWithIncludes from fail2ban.client.filterreader import FilterReader
from fail2ban.server.filter import Filter from fail2ban.server.filter import Filter
from fail2ban.server.failregex import RegexException from fail2ban.server.failregex import RegexException
@ -206,8 +206,6 @@ class LineStats(object):
class Fail2banRegex(object): class Fail2banRegex(object):
CONFIG_DEFAULTS = {'configpath' : "/etc/fail2ban/"}
def __init__(self, opts): def __init__(self, opts):
self._verbose = opts.verbose self._verbose = opts.verbose
self._debuggex = opts.debuggex self._debuggex = opts.debuggex
@ -257,46 +255,37 @@ class Fail2banRegex(object):
assert(regextype in ('fail', 'ignore')) assert(regextype in ('fail', 'ignore'))
regex = regextype + 'regex' regex = regextype + 'regex'
if os.path.isfile(value): if os.path.isfile(value):
reader = SafeConfigParserWithIncludes(defaults=self.CONFIG_DEFAULTS)
try:
reader.read(value)
print "Use %11s file : %s" % (regex, value) print "Use %11s file : %s" % (regex, value)
# TODO: reuse functionality in client reader = FilterReader(value, 'fail2ban-regex-jail', {})
regex_values = [ reader.setBaseDir(None)
RegexStat(m)
for m in reader.get("Definition", regex).split('\n')
if m != ""]
except NoSectionError:
print "No [Definition] section in %s" % value
return False
except NoOptionError:
print "No %s option in %s" % (regex, value)
return False
except MissingSectionHeaderError:
print "No section headers in %s" % value
return False
if reader.readexplicit():
reader.getOptions(None)
readercommands = reader.convert()
regex_values = [
RegexStat(m[3])
for m in filter(
lambda x: x[0] == 'set' and x[2] == "add%sregex" % regextype,
readercommands)]
# Read out and set possible value of maxlines # Read out and set possible value of maxlines
try: for command in readercommands:
maxlines = reader.get("Init", "maxlines") if command[2] == "maxlines":
except (NoSectionError, NoOptionError): maxlines = int(command[3])
# No [Init].maxlines found.
pass
else:
try: try:
self.setMaxLines(maxlines) self.setMaxLines(maxlines)
except ValueError: except ValueError:
print "ERROR: Invalid value for maxlines (%(maxlines)r) " \ print "ERROR: Invalid value for maxlines (%(maxlines)r) " \
"read from %(value)s" % locals() "read from %(value)s" % locals()
return False return False
# Read out and set possible value for journalmatch elif command[2] == 'addjournalmatch':
try: journalmatch = command[3]
journalmatch = reader.get("Init", "journalmatch")
except (NoSectionError, NoOptionError):
# No [Init].journalmatch found.
pass
else:
self.setJournalMatch(shlex.split(journalmatch)) self.setJournalMatch(shlex.split(journalmatch))
elif command[2] == 'datepattern':
datepattern = command[3]
self.setDatePattern(datepattern)
else:
print "ERROR: failed to read %s" % value
return False
else: else:
print "Use %11s line : %s" % (regex, shortstr(value)) print "Use %11s line : %s" % (regex, shortstr(value))
regex_values = [RegexStat(value)] regex_values = [RegexStat(value)]

View File

@ -0,0 +1,69 @@
# Fail2Ban action file for firewall-cmd/ipset
#
# This requires:
# ipset (package: ipset)
# firewall-cmd (package: firewalld)
#
# This is for ipset protocol 6 (and hopefully later) (ipset v6.14).
# Use ipset -V to see the protocol and version.
#
# 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-blocktype.conf
[Definition]
actionstart = ipset create fail2ban-<name> hash:ip timeout <bantime>
firewall-cmd --direct --add-rule ipv4 filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
actionstop = firewall-cmd --direct --remove-rule ipv4 filter <chain> 0 -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype>
ipset flush fail2ban-<name>
ipset destroy fail2ban-<name>
actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q '^fail2ban-<name>$'
actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
actionunban = ipset del fail2ban-<name> <ip> -exist
[Init]
# Default name of the chain
#
name = default
# Option: port
# Notes.: specifies port to monitor
# Values: [ NUM | STRING ]
#
port = ssh
# Option: protocol
# Notes.: internally used by config reader for interpolations.
# Values: [ tcp | udp | icmp | all ]
#
protocol = tcp
# Option: chain
# Notes specifies the iptables chain to which the fail2ban rules should be
# added
# Values: [ STRING ]
#
chain = INPUT_direct
# Option: bantime
# Notes: specifies the bantime in seconds (handled internally rather than by fail2ban)
# Values: [ NUM ] Default: 600
bantime = 600
# DEV NOTES:
#
# Author: Edgar Hoch and Daniel Black
# firewallcmd-new / iptables-ipset-proto6 combined for maximium goodness

View File

@ -8,19 +8,19 @@ before = iptables-blocktype.conf
[Definition] [Definition]
actionstart = firewall-cmd --direct --add-chain ipv4 filter fail2ban-<name> actionstart = firewall-cmd --direct --add-chain ipv4 filter f2b-<name>
firewall-cmd --direct --add-rule ipv4 filter fail2ban-<name> 1000 -j RETURN firewall-cmd --direct --add-rule ipv4 filter f2b-<name> 1000 -j RETURN
firewall-cmd --direct --add-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name> firewall-cmd --direct --add-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
actionstop = firewall-cmd --direct --remove-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name> actionstop = firewall-cmd --direct --remove-rule ipv4 filter <chain> 0 -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
firewall-cmd --direct --remove-rules ipv4 filter fail2ban-<name> firewall-cmd --direct --remove-rules ipv4 filter f2b-<name>
firewall-cmd --direct --remove-chain ipv4 filter fail2ban-<name> firewall-cmd --direct --remove-chain ipv4 filter f2b-<name>
actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q '^fail2ban-<name>$' actioncheck = firewall-cmd --direct --get-chains ipv4 filter | grep -q 'f2b-<name>$'
actionban = firewall-cmd --direct --add-rule ipv4 filter fail2ban-<name> 0 -s <ip> -j <blocktype> actionban = firewall-cmd --direct --add-rule ipv4 filter f2b-<name> 0 -s <ip> -j <blocktype>
actionunban = firewall-cmd --direct --remove-rule ipv4 filter fail2ban-<name> 0 -s <ip> -j <blocktype> actionunban = firewall-cmd --direct --remove-rule ipv4 filter f2b-<name> 0 -s <ip> -j <blocktype>
[Init] [Init]

View File

@ -17,23 +17,23 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = iptables -N fail2ban-<name> actionstart = iptables -N f2b-<name>
iptables -A fail2ban-<name> -j RETURN iptables -A f2b-<name> -j RETURN
iptables -I <chain> -p <protocol> -j fail2ban-<name> iptables -I <chain> -p <protocol> -j f2b-<name>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D <chain> -p <protocol> -j fail2ban-<name> actionstop = iptables -D <chain> -p <protocol> -j f2b-<name>
iptables -F fail2ban-<name> iptables -F f2b-<name>
iptables -X fail2ban-<name> iptables -X f2b-<name>
# Option: actioncheck # Option: actioncheck
# 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 'fail2ban-<name>[ \t]' actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
# 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
@ -41,7 +41,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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
@ -49,7 +49,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype> actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
[Init] [Init]

View File

@ -27,16 +27,16 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = ipset --create fail2ban-<name> iphash actionstart = ipset --create f2b-<name> iphash
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype> iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype> actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
ipset --flush fail2ban-<name> ipset --flush f2b-<name>
ipset --destroy fail2ban-<name> ipset --destroy f2b-<name>
# 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
@ -44,7 +44,7 @@ actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = ipset --test fail2ban-<name> <ip> || ipset --add fail2ban-<name> <ip> actionban = ipset --test f2b-<name> <ip> || ipset --add f2b-<name> <ip>
# 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
@ -52,7 +52,7 @@ actionban = ipset --test fail2ban-<name> <ip> || ipset --add fail2ban-<name> <i
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = ipset --test fail2ban-<name> <ip> && ipset --del fail2ban-<name> <ip> actionunban = ipset --test f2b-<name> <ip> && ipset --del f2b-<name> <ip>
[Init] [Init]

View File

@ -24,16 +24,16 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = ipset create fail2ban-<name> hash:ip timeout <bantime> actionstart = ipset create f2b-<name> hash:ip timeout <bantime>
iptables -I INPUT -m set --match-set fail2ban-<name> src -j <blocktype> iptables -I INPUT -m set --match-set f2b-<name> src -j <blocktype>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D INPUT -m set --match-set fail2ban-<name> src -j <blocktype> actionstop = iptables -D INPUT -m set --match-set f2b-<name> src -j <blocktype>
ipset flush fail2ban-<name> ipset flush f2b-<name>
ipset destroy fail2ban-<name> ipset destroy f2b-<name>
# 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
@ -41,7 +41,7 @@ actionstop = iptables -D INPUT -m set --match-set fail2ban-<name> src -j <blockt
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
# 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
@ -49,7 +49,7 @@ actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = ipset del fail2ban-<name> <ip> -exist actionunban = ipset del f2b-<name> <ip> -exist
[Init] [Init]

View File

@ -24,16 +24,16 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = ipset create fail2ban-<name> hash:ip timeout <bantime> actionstart = ipset create f2b-<name> hash:ip timeout <bantime>
iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype> iptables -I INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set fail2ban-<name> src -j <blocktype> actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set --match-set f2b-<name> src -j <blocktype>
ipset flush fail2ban-<name> ipset flush f2b-<name>
ipset destroy fail2ban-<name> ipset destroy f2b-<name>
# 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
@ -41,7 +41,7 @@ actionstop = iptables -D INPUT -p <protocol> -m multiport --dports <port> -m set
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist actionban = ipset add f2b-<name> <ip> timeout <bantime> -exist
# 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
@ -49,7 +49,7 @@ actionban = ipset add fail2ban-<name> <ip> timeout <bantime> -exist
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = ipset del fail2ban-<name> <ip> -exist actionunban = ipset del f2b-<name> <ip> -exist
[Init] [Init]

View File

@ -3,9 +3,9 @@
# Author: Guido Bozzetto # Author: Guido Bozzetto
# Modified: Cyril Jaquier # Modified: Cyril Jaquier
# #
# make "fail2ban-<name>" chain to match drop IP # make "f2b-<name>" chain to match drop IP
# make "fail2ban-<name>-log" chain to log and drop # make "f2b-<name>-log" chain to log and drop
# insert a jump to fail2ban-<name> from -I <chain> if proto/port match # insert a jump to f2b-<name> from -I <chain> if proto/port match
# #
# #
@ -19,28 +19,28 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = iptables -N fail2ban-<name> actionstart = iptables -N f2b-<name>
iptables -A fail2ban-<name> -j RETURN iptables -A f2b-<name> -j RETURN
iptables -I <chain> 1 -p <protocol> -m multiport --dports <port> -j fail2ban-<name> iptables -I <chain> 1 -p <protocol> -m multiport --dports <port> -j f2b-<name>
iptables -N fail2ban-<name>-log iptables -N f2b-<name>-log
iptables -I fail2ban-<name>-log -j LOG --log-prefix "$(expr fail2ban-<name> : '\(.\{1,23\}\)'):DROP " --log-level warning -m limit --limit 6/m --limit-burst 2 iptables -I f2b-<name>-log -j LOG --log-prefix "$(expr f2b-<name> : '\(.\{1,23\}\)'):DROP " --log-level warning -m limit --limit 6/m --limit-burst 2
iptables -A fail2ban-<name>-log -j <blocktype> iptables -A f2b-<name>-log -j <blocktype>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name> actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
iptables -F fail2ban-<name> iptables -F f2b-<name>
iptables -F fail2ban-<name>-log iptables -F f2b-<name>-log
iptables -X fail2ban-<name> iptables -X f2b-<name>
iptables -X fail2ban-<name>-log iptables -X f2b-<name>-log
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
# Values: CMD # Values: CMD
# #
actioncheck = iptables -n -L fail2ban-<name>-log >/dev/null actioncheck = iptables -n -L f2b-<name>-log >/dev/null
# 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,7 +48,7 @@ actioncheck = iptables -n -L fail2ban-<name>-log >/dev/null
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j fail2ban-<name>-log actionban = iptables -I f2b-<name> 1 -s <ip> -j f2b-<name>-log
# 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
@ -56,7 +56,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j fail2ban-<name>-log
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = iptables -D fail2ban-<name> -s <ip> -j fail2ban-<name>-log actionunban = iptables -D f2b-<name> -s <ip> -j f2b-<name>-log
[Init] [Init]

View File

@ -14,23 +14,23 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = iptables -N fail2ban-<name> actionstart = iptables -N f2b-<name>
iptables -A fail2ban-<name> -j RETURN iptables -A f2b-<name> -j RETURN
iptables -I <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name> iptables -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j fail2ban-<name> actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>
iptables -F fail2ban-<name> iptables -F f2b-<name>
iptables -X fail2ban-<name> iptables -X f2b-<name>
# Option: actioncheck # Option: actioncheck
# 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 'fail2ban-<name>[ \t]' actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
# 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
@ -38,7 +38,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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
@ -46,7 +46,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype> actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
[Init] [Init]

View File

@ -17,23 +17,23 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = iptables -N fail2ban-<name> actionstart = iptables -N f2b-<name>
iptables -A fail2ban-<name> -j RETURN iptables -A f2b-<name> -j RETURN
iptables -I <chain> -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name> iptables -I <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D <chain> -m state --state NEW -p <protocol> --dport <port> -j fail2ban-<name> actionstop = iptables -D <chain> -m state --state NEW -p <protocol> --dport <port> -j f2b-<name>
iptables -F fail2ban-<name> iptables -F f2b-<name>
iptables -X fail2ban-<name> iptables -X f2b-<name>
# Option: actioncheck # Option: actioncheck
# 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 'fail2ban-<name>[ \t]' actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
# 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
@ -41,7 +41,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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
@ -49,7 +49,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype> actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
[Init] [Init]

View File

@ -23,29 +23,29 @@ before = iptables-blocktype.conf
# iptables-persistent package). # iptables-persistent package).
# #
# Explanation of the rule below: # Explanation of the rule below:
# Check if any packets coming from an IP on the fail2ban-<name> # Check if any packets coming from an IP on the f2b-<name>
# list have been seen in the last 3600 seconds. If yes, update the # list have been seen in the last 3600 seconds. If yes, update the
# timestamp for this IP and drop the packet. If not, let the packet # timestamp for this IP and drop the packet. If not, let the packet
# through. # through.
# #
# Fail2ban inserts blacklisted hosts into the fail2ban-<name> list # Fail2ban inserts blacklisted hosts into the f2b-<name> list
# and removes them from the list after some time, according to its # and removes them from the list after some time, according to its
# 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 = iptables -I INPUT -m recent --update --seconds 3600 --name fail2ban-<name> -j <blocktype> actionstart = iptables -I INPUT -m recent --update --seconds 3600 --name f2b-<name> -j <blocktype>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = echo / > /proc/net/xt_recent/fail2ban-<name> actionstop = echo / > /proc/net/xt_recent/f2b-<name>
# Option: actioncheck # Option: actioncheck
# Notes.: command executed once before each actionban command # Notes.: command executed once before each actionban command
# Values: CMD # Values: CMD
# #
actioncheck = test -e /proc/net/xt_recent/fail2ban-<name> actioncheck = test -e /proc/net/xt_recent/f2b-<name>
# 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
@ -53,7 +53,7 @@ actioncheck = test -e /proc/net/xt_recent/fail2ban-<name>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = echo +<ip> > /proc/net/xt_recent/fail2ban-<name> actionban = echo +<ip> > /proc/net/xt_recent/f2b-<name>
# 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
@ -61,7 +61,7 @@ actionban = echo +<ip> > /proc/net/xt_recent/fail2ban-<name>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = echo -<ip> > /proc/net/xt_recent/fail2ban-<name> actionunban = echo -<ip> > /proc/net/xt_recent/f2b-<name>
[Init] [Init]

View File

@ -14,23 +14,23 @@ before = iptables-blocktype.conf
# Notes.: command executed once at the start of Fail2Ban. # Notes.: command executed once at the start of Fail2Ban.
# Values: CMD # Values: CMD
# #
actionstart = iptables -N fail2ban-<name> actionstart = iptables -N f2b-<name>
iptables -A fail2ban-<name> -j RETURN iptables -A f2b-<name> -j RETURN
iptables -I <chain> -p <protocol> --dport <port> -j fail2ban-<name> iptables -I <chain> -p <protocol> --dport <port> -j f2b-<name>
# Option: actionstop # Option: actionstop
# Notes.: command executed once at the end of Fail2Ban # Notes.: command executed once at the end of Fail2Ban
# Values: CMD # Values: CMD
# #
actionstop = iptables -D <chain> -p <protocol> --dport <port> -j fail2ban-<name> actionstop = iptables -D <chain> -p <protocol> --dport <port> -j f2b-<name>
iptables -F fail2ban-<name> iptables -F f2b-<name>
iptables -X fail2ban-<name> iptables -X f2b-<name>
# Option: actioncheck # Option: actioncheck
# 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 'fail2ban-<name>[ \t]' actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>[ \t]'
# 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
@ -38,7 +38,7 @@ actioncheck = iptables -n -L <chain> | grep -q 'fail2ban-<name>[ \t]'
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype> actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype>
# 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
@ -46,7 +46,7 @@ actionban = iptables -I fail2ban-<name> 1 -s <ip> -j <blocktype>
# Tags: See jail.conf(5) man page # Tags: See jail.conf(5) man page
# Values: CMD # Values: CMD
# #
actionunban = iptables -D fail2ban-<name> -s <ip> -j <blocktype> actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype>
[Init] [Init]

225
config/action.d/smtp.py Normal file
View File

@ -0,0 +1,225 @@
# 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
import socket
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate, formataddr
from fail2ban.server.actions import ActionBase, CallingMap
messages = {}
messages['start'] = \
"""Hi,
The jail %(jailname)s has been started successfully.
Regards,
Fail2Ban"""
messages['stop'] = \
"""Hi,
The jail %(jailname)s has been stopped.
Regards,
Fail2Ban"""
messages['ban'] = {}
messages['ban']['head'] = \
"""Hi,
The IP %(ip)s has just been banned for %(bantime)s seconds
by Fail2Ban after %(failures)i attempts against %(jailname)s.
"""
messages['ban']['tail'] = \
"""
Regards,
Fail2Ban"""
messages['ban']['matches'] = \
"""
Matches for this ban:
%(matches)s
"""
messages['ban']['ipmatches'] = \
"""
Matches for %(ip)s:
%(ipmatches)s
"""
messages['ban']['ipjailmatches'] = \
"""
Matches for %(ip)s for jail %(jailname)s:
%(ipjailmatches)s
"""
class SMTPAction(ActionBase):
"""Fail2Ban action which sends emails to inform on jail starting,
stopping and bans.
"""
def __init__(
self, jail, name, host="localhost", user=None, password=None,
sendername="Fail2Ban", sender="fail2ban", dest="root", matches=None):
"""Initialise action.
Parameters
----------
jail : Jail
The jail which the action belongs to.
name : str
Named assigned to the action.
host : str, optional
SMTP host, of host:port format. Default host "localhost" and
port "25"
user : str, optional
Username used for authentication with SMTP server.
password : str, optional
Password used for authentication with SMTP server.
sendername : str, optional
Name to use for from address in email. Default "Fail2Ban".
sender : str, optional
Email address to use for from address in email.
Default "fail2ban".
dest : str, optional
Email addresses of intended recipient(s) in comma delimited
format. Default "root".
matches : str, optional
Type of matches to be included from ban in email. Can be one
of "matches", "ipmatches" or "ipjailmatches". Default None
(see man jail.conf.5).
"""
super(SMTPAction, self).__init__(jail, name)
self.host = host
#TODO: self.ssl = ssl
self.user = user
self.password =password
self.fromname = sendername
self.fromaddr = sender
self.toaddr = dest
self.matches = matches
self.message_values = CallingMap(
jailname = self._jail.getName(), # Doesn't change
hostname = socket.gethostname,
bantime = self._jail.actions.getBanTime,
)
def _sendMessage(self, subject, text):
"""Sends message based on arguments and instance's properties.
Parameters
----------
subject : str
Subject of the email.
text : str
Body of the email.
Raises
------
SMTPConnectionError
Error on connecting to host.
SMTPAuthenticationError
Error authenticating with SMTP server.
SMTPException
See Python `smtplib` for full list of other possible
exceptions.
"""
msg = MIMEText(text)
msg['Subject'] = subject
msg['From'] = formataddr((self.fromname, self.fromaddr))
msg['To'] = self.toaddr
msg['Date'] = formatdate()
smtp = smtplib.SMTP()
try:
self._logSys.debug("Connected to SMTP '%s', response: %i: %s",
self.host, *smtp.connect(self.host))
if self.user and self.password:
smtp.login(self.user, self.password)
failed_recipients = smtp.sendmail(
self.fromaddr, self.toaddr, msg.as_string())
except smtplib.SMTPConnectError:
self._logSys.error("Error connecting to host '%s'", self.host)
raise
except smtplib.SMTPAuthenticationError:
self._logSys.error(
"Failed to authenticate with host '%s' user '%s'",
self.host, self.user)
raise
except smtplib.SMTPException:
self._logSys.error(
"Error sending mail to host '%s' from '%s' to '%s'",
self.host, self.fromaddr, self.toaddr)
raise
else:
if failed_recipients:
self._logSys.warning(
"Email to '%s' failed to following recipients: %r",
self.toaddr, failed_recipients)
self._logSys.debug("Email '%s' successfully sent", subject)
finally:
try:
self._logSys.debug("Disconnected from '%s', response %i: %s",
self.host, *smtp.quit())
except smtplib.SMTPServerDisconnected:
pass # Not connected
def start(self):
"""Sends email to recipients informing that the jail has started.
"""
self._sendMessage(
"[Fail2Ban] %(jailname)s: started on %(hostname)s" %
self.message_values,
messages['start'] % self.message_values)
def stop(self):
"""Sends email to recipients informing that the jail has stopped.
"""
self._sendMessage(
"[Fail2Ban] %(jailname)s: stopped on %(hostname)s" %
self.message_values,
messages['stop'] % self.message_values)
def ban(self, aInfo):
"""Sends email to recipients informing that ban has occurred.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
aInfo.update(self.message_values)
message = "".join([
messages['ban']['head'],
messages['ban'].get(self.matches, ""),
messages['ban']['tail']
])
self._sendMessage(
"[Fail2Ban] %(jailname)s: banned %(ip)s from %(hostname)s" %
aInfo,
message % aInfo)
Action = SMTPAction

40
config/action.d/ufw.conf Normal file
View File

@ -0,0 +1,40 @@
# Fail2Ban action configuration file for ufw
#
# You are required to run "ufw enable" before this will have an effect.
#
# The insert position should be approprate to block the required traffic.
# A number after an allow rule to the application won't be much use.
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = [ -n "<application>" ] && app="app <application>" ; ufw insert <insertpos> <blocktype> from <ip> to <destination> $app
actionunban = [ -n "<application>" ] && app="app <application>" ; ufw delete <blocktype> from <ip> to <destination> $app
[Init]
# Option: insertpos
# Notes.: The postition number in the firewall list to insert the block rule
insertpos = 1
# Option: blocktype
# Notes.: reject or deny
blocktype = reject
# Option: destination
# Notes.: The destination address to block in the ufw rule
destination = any
# Option: application
# Notes.: application from sudo ufw app list
application =
# DEV NOTES:
#
# Author: Guilhem Lettron
# Enhancements: Daniel Black

View File

@ -11,7 +11,12 @@
# #
# honeypot: :blackhole: # honeypot: :blackhole:
# #
# The use the jail.local filter = exim-spam[honeypot=honeypot@yourdomain.com] # For the SA: Action: silently tossed message... to be logged exim's SAdevnull option needs to be used.
#
# To this filter use the jail.local should contain in the right jail:
#
# filter = exim-spam[honeypot=honeypot@yourdomain.com]
#
[INCLUDES] [INCLUDES]
@ -25,6 +30,7 @@ failregex = ^%(pid)s \S+ F=(<>|\S+@\S+) %(host_info)srejected by local_scan\(\)
^%(pid)s %(host_info)sF=(<>|[^@]+@\S+) rejected RCPT [^@]+@\S+: .*dnsbl.*\s*$ ^%(pid)s %(host_info)sF=(<>|[^@]+@\S+) rejected RCPT [^@]+@\S+: .*dnsbl.*\s*$
^%(pid)s \S+ %(host_info)sF=(<>|[^@]+@\S+) rejected after DATA: This message contains a virus \(\S+\)\.\s*$ ^%(pid)s \S+ %(host_info)sF=(<>|[^@]+@\S+) rejected after DATA: This message contains a virus \(\S+\)\.\s*$
^%(pid)s \S+ SA: Action: flagged as Spam but accepted: score=\d+\.\d+ required=\d+\.\d+ \(scanned in \d+/\d+ secs \| Message-Id: \S+\)\. From \S+ \(host=\S+ \[<HOST>\]\) for <honeypot>$ ^%(pid)s \S+ SA: Action: flagged as Spam but accepted: score=\d+\.\d+ required=\d+\.\d+ \(scanned in \d+/\d+ secs \| Message-Id: \S+\)\. From \S+ \(host=\S+ \[<HOST>\]\) for <honeypot>$
^%(pid)s \S+ SA: Action: silently tossed message: score=\d+\.\d+ required=\d+\.\d+ trigger=\d+\.\d+ \(scanned in \d+/\d+ secs \| Message-Id: \S+\)\. From \S+ \(host=(\S+ )?\[<HOST>\]\) for \S+$
ignoreregex = ignoreregex =

View File

@ -0,0 +1,16 @@
# fail2ban filter configuration for horde
[Definition]
failregex = ^ HORDE \[error\] \[(horde|imp)\] FAILED LOGIN for \S+ \[<HOST>\](\(forwarded for \[\S+\]\))? to (Horde|{[^}]+}) \[(pid \d+ )?on line \d+ of \S+\]$
ignoreregex =
# DEV NOTES:
# https://github.com/horde/horde/blob/master/imp/lib/Auth.php#L132
# https://github.com/horde/horde/blob/master/horde/login.php
#
# Author: Daniel Black

View File

@ -0,0 +1,15 @@
# Fail2Ban filter for Openwebmail
# banning hosts with authentication errors in /var/log/openwebmail.log
# OpenWebMail http://openwebmail.org
#
[Definition]
failregex = ^ - \[\d+\] \(<HOST>\) (?P<USER>\S+) - login error - (no such user - loginname=(?P=USER)|auth_unix.pl, ret -4, Password incorrect)$
^ - \[\d+\] \(<HOST>\) (?P<USER>\S+) - userinfo error - auth_unix.pl, ret -4, User (?P=USER) doesn't exist$
ignoreregex =
# DEV Notes:
#
# Author: Ivo Truxa (c) 2013 truXoft.com

View File

@ -0,0 +1,11 @@
# Fail2ban filter for stunnel
[Definition]
failregex = ^ LOG\d\[\d+:\d+\]:\ SSL_accept from <HOST>:\d+ : (?P<CODE>[\dA-F]+): error:(?P=CODE):SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate$
# DEV NOTES:
#
# Author: Daniel Black
#
# Based off: http://www.fail2ban.org/wiki/index.php/Fail2ban:Community_Portal#stunnel4

View File

@ -390,6 +390,11 @@ port = http,https
logpath = /var/log/roundcube/userlogins logpath = /var/log/roundcube/userlogins
[openwebmail]
port = http,https
logpath = /var/log/openwebmail.log`
[sogo-auth] [sogo-auth]
# Monitor SOGo groupware server # Monitor SOGo groupware server
# without proxy this would be: # without proxy this would be:

View File

@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, os import logging, os
from configreader import ConfigReader, DefinitionInitConfigReader
from .configreader import ConfigReader, DefinitionInitConfigReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -58,22 +59,20 @@ class ActionReader(DefinitionInitConfigReader):
head = ["set", self._jailName] head = ["set", self._jailName]
stream = list() stream = list()
stream.append(head + ["addaction", self._name]) stream.append(head + ["addaction", self._name])
head.extend(["action", self._name])
for opt in self._opts: for opt in self._opts:
if opt == "actionstart": if opt == "actionstart":
stream.append(head + ["actionstart", self._name, self._opts[opt]]) stream.append(head + ["actionstart", self._opts[opt]])
elif opt == "actionstop": elif opt == "actionstop":
stream.append(head + ["actionstop", self._name, self._opts[opt]]) stream.append(head + ["actionstop", self._opts[opt]])
elif opt == "actioncheck": elif opt == "actioncheck":
stream.append(head + ["actioncheck", self._name, self._opts[opt]]) stream.append(head + ["actioncheck", self._opts[opt]])
elif opt == "actionban": elif opt == "actionban":
stream.append(head + ["actionban", self._name, self._opts[opt]]) stream.append(head + ["actionban", self._opts[opt]])
elif opt == "actionunban": elif opt == "actionunban":
stream.append(head + ["actionunban", self._name, self._opts[opt]]) stream.append(head + ["actionunban", self._opts[opt]])
if self._initOpts: if self._initOpts:
if "timeout" in self._initOpts:
stream.append(head + ["timeout", self._name, self._opts["timeout"]])
# cInfo
for p in self._initOpts: for p in self._initOpts:
stream.append(head + ["setcinfo", self._name, p, self._initOpts[p]]) stream.append(head + [p, self._initOpts[p]])
return stream return stream

View File

@ -23,7 +23,7 @@ __license__ = "GPL"
import logging import logging
from fail2ban.exceptions import UnknownJailException, DuplicateJailException from ..exceptions import UnknownJailException, DuplicateJailException
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -165,7 +165,23 @@ class Beautifier:
msg = "No actions for jail %s" % inC[1] msg = "No actions for jail %s" % inC[1]
else: else:
msg = "The jail %s has the following actions:\n" % inC[1] msg = "The jail %s has the following actions:\n" % inC[1]
msg += ", ".join(action.getName() for action in response) msg += ", ".join(response)
elif inC[2] == "actionproperties":
if len(response) == 0:
msg = "No properties for jail %s action %s" % (
inC[1], inC[3])
else:
msg = "The jail %s action %s has the following " \
"properties:\n" % (inC[1], inC[3])
msg += ", ".join(response)
elif inC[2] == "actionmethods":
if len(response) == 0:
msg = "No methods for jail %s action %s" % (
inC[1], inC[3])
else:
msg = "The jail %s action %s has the following " \
"methods:\n" % (inC[1], inC[3])
msg += ", ".join(response)
except Exception: except Exception:
logSys.warning("Beautifier error. Please report the error") logSys.warning("Beautifier error. Please report the error")
logSys.error("Beautify " + `response` + " with " + `self.__inputCmd` + logSys.error("Beautify " + `response` + " with " + `self.__inputCmd` +

View File

@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import glob, logging, os import glob, logging, os
from configparserinc import SafeConfigParserWithIncludes
from ConfigParser import NoOptionError, NoSectionError from ConfigParser import NoOptionError, NoSectionError
from .configparserinc import SafeConfigParserWithIncludes
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -158,6 +159,10 @@ class DefinitionInitConfigReader(ConfigReader):
def read(self): def read(self):
return ConfigReader.read(self, self._file) return ConfigReader.read(self, self._file)
# needed for fail2ban-regex that doesn't need fancy directories
def readexplicit(self):
return SafeConfigParserWithIncludes.read(self, self._file)
def getOptions(self, pOpts): def getOptions(self, pOpts):
self._opts = ConfigReader.getOptions( self._opts = ConfigReader.getOptions(
self, "Definition", self._configOpts, pOpts) self, "Definition", self._configOpts, pOpts)

View File

@ -25,9 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from fail2banreader import Fail2banReader from .configreader import ConfigReader
from jailsreader import JailsReader from .fail2banreader import Fail2banReader
from .jailsreader import JailsReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,7 +25,8 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from .configreader import ConfigReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,8 +25,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, os, shlex import logging, os, shlex
from configreader import ConfigReader, DefinitionInitConfigReader
from fail2ban.server.action import Action from .configreader import ConfigReader, DefinitionInitConfigReader
from ..server.action import CommandAction
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -44,7 +45,7 @@ class FilterReader(DefinitionInitConfigReader):
def convert(self): def convert(self):
stream = list() stream = list()
combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items())) combinedopts = dict(list(self._opts.items()) + list(self._initOpts.items()))
opts = Action.substituteRecursiveTags(combinedopts) opts = CommandAction.substituteRecursiveTags(combinedopts)
if not opts: if not opts:
raise ValueError('recursive tag definitions unable to be resolved') raise ValueError('recursive tag definitions unable to be resolved')
for opt, value in opts.iteritems(): for opt, value in opts.iteritems():

View File

@ -25,10 +25,11 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, re, glob, os.path import logging, re, glob, os.path
import json
from configreader import ConfigReader from .configreader import ConfigReader
from filterreader import FilterReader from .filterreader import FilterReader
from actionreader import ActionReader from .actionreader import ActionReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -120,8 +121,20 @@ class JailReader(ConfigReader):
if not act: # skip empty actions if not act: # skip empty actions
continue continue
actName, actOpt = JailReader.extractOptions(act) actName, actOpt = JailReader.extractOptions(act)
if actName.endswith(".py"):
self.__actions.append([
"set",
self.__name,
"addaction",
actOpt.pop("actname", os.path.splitext(actName)[0]),
os.path.join(
self.getBaseDir(), "action.d", actName),
json.dumps(actOpt),
])
else:
action = ActionReader( action = ActionReader(
actName, self.__name, actOpt, basedir=self.getBaseDir()) actName, self.__name, actOpt,
basedir=self.getBaseDir())
ret = action.read() ret = action.read()
if ret: if ret:
action.getOptions(self.__opts) action.getOptions(self.__opts)
@ -153,7 +166,7 @@ class JailReader(ConfigReader):
found_files = 0 found_files = 0
for path in self.__opts[opt].split("\n"): for path in self.__opts[opt].split("\n"):
path = path.rsplit(" ", 1) path = path.rsplit(" ", 1)
path, tail = path if len(path) > 1 else (path[0], "false") path, tail = path if len(path) > 1 else (path[0], "head")
pathList = JailReader._glob(path) pathList = JailReader._glob(path)
if len(pathList) == 0: if len(pathList) == 0:
logSys.error("No file(s) found for glob %s" % path) logSys.error("No file(s) found for glob %s" % path)
@ -193,7 +206,10 @@ class JailReader(ConfigReader):
if self.__filter: if self.__filter:
stream.extend(self.__filter.convert()) stream.extend(self.__filter.convert())
for action in self.__actions: for action in self.__actions:
if isinstance(action, ConfigReader):
stream.extend(action.convert()) stream.extend(action.convert())
else:
stream.append(action)
stream.insert(0, ["add", self.__name, backend]) stream.insert(0, ["add", self.__name, backend])
return stream return stream

View File

@ -25,8 +25,9 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging import logging
from configreader import ConfigReader
from jailreader import JailReader from .configreader import ConfigReader
from .jailreader import JailReader
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -29,7 +29,7 @@ __license__ = "GPL"
class DuplicateJailException(Exception): class DuplicateJailException(Exception):
pass pass
class UnknownJailException(Exception): class UnknownJailException(KeyError):
pass pass

View File

@ -76,19 +76,21 @@ protocol = [
["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"], ["set <JAIL> unbanip <IP>", "manually Unban <IP> in <JAIL>"],
["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"], ["set <JAIL> maxretry <RETRY>", "sets the number of failures <RETRY> before banning the host for <JAIL>"],
["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"], ["set <JAIL> maxlines <LINES>", "sets the number of <LINES> to buffer for regex search for <JAIL>"],
["set <JAIL> addaction <ACT>", "adds a new action named <NAME> for <JAIL>"], ["set <JAIL> addaction <ACT>[ <PYTHONFILE> <JSONKWARGS>]", "adds a new action named <NAME> for <JAIL>. Optionally for a Python based action, a <PYTHONFILE> and <JSONKWARGS> can be specified, else will be a Command Action"],
["set <JAIL> delaction <ACT>", "removes the action <NAME> from <JAIL>"], ["set <JAIL> delaction <ACT>", "removes the action <ACT> from <JAIL>"],
["set <JAIL> setcinfo <ACT> <KEY> <VALUE>", "sets <VALUE> for <KEY> of the action <NAME> for <JAIL>"], ["", "COMMAND ACTION CONFIGURATION", ""],
["set <JAIL> delcinfo <ACT> <KEY>", "removes <KEY> for the action <NAME> for <JAIL>"], ["set <JAIL> action <ACT> actionstart <CMD>", "sets the start command <CMD> of the action <ACT> for <JAIL>"],
["set <JAIL> timeout <ACT> <TIMEOUT>", "sets <TIMEOUT> as the command timeout in seconds for the action <ACT> for <JAIL>"], ["set <JAIL> action <ACT> actionstop <CMD>", "sets the stop command <CMD> of the action <ACT> for <JAIL>"],
["set <JAIL> actionstart <ACT> <CMD>", "sets the start command <CMD> of the action <ACT> for <JAIL>"], ["set <JAIL> action <ACT> actioncheck <CMD>", "sets the check command <CMD> of the action <ACT> for <JAIL>"],
["set <JAIL> actionstop <ACT> <CMD>", "sets the stop command <CMD> of the action <ACT> for <JAIL>"], ["set <JAIL> action <ACT> actionban <CMD>", "sets the ban command <CMD> of the action <ACT> for <JAIL>"],
["set <JAIL> actioncheck <ACT> <CMD>", "sets the check command <CMD> of the action <ACT> for <JAIL>"], ["set <JAIL> action <ACT> actionunban <CMD>", "sets the unban command <CMD> of the action <ACT> for <JAIL>"],
["set <JAIL> actionban <ACT> <CMD>", "sets the ban command <CMD> of the action <ACT> for <JAIL>"], ["set <JAIL> action <ACT> timeout <TIMEOUT>", "sets <TIMEOUT> as the command timeout in seconds for the action <ACT> for <JAIL>"],
["set <JAIL> actionunban <ACT> <CMD>", "sets the unban command <CMD> of the action <ACT> for <JAIL>"], ["", "GENERAL ACTION CONFIGURATION", ""],
["set <JAIL> action <ACT> <PROPERTY> <VALUE>", "sets the <VALUE> of <PROPERTY> for the action <ACT> for <JAIL>"],
["set <JAIL> action <ACT> <METHOD>[ <JSONKWARGS>]", "calls the <METHOD> with <JSONKWARGS> for the action <ACT> for <JAIL>"],
['', "JAIL INFORMATION", ""], ['', "JAIL INFORMATION", ""],
["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"], ["get <JAIL> logpath", "gets the list of the monitored files for <JAIL>"],
["get <JAIL> logencoding <ENCODING>", "gets the <ENCODING> of the log files for <JAIL>"], ["get <JAIL> logencoding", "gets the encoding of the log files for <JAIL>"],
["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"], ["get <JAIL> journalmatch", "gets the journal filter match for <JAIL>"],
["get <JAIL> ignoreip", "gets the list of ignored IP addresses for <JAIL>"], ["get <JAIL> ignoreip", "gets the list of ignored IP addresses for <JAIL>"],
["get <JAIL> ignorecommand", "gets ignorecommand of <JAIL>"], ["get <JAIL> ignorecommand", "gets ignorecommand of <JAIL>"],
@ -100,15 +102,18 @@ protocol = [
["get <JAIL> usedns", "gets the usedns setting for <JAIL>"], ["get <JAIL> usedns", "gets the usedns setting for <JAIL>"],
["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"], ["get <JAIL> maxretry", "gets the number of failures allowed for <JAIL>"],
["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"], ["get <JAIL> maxlines", "gets the number of lines to buffer for <JAIL>"],
["get <JAIL> addaction", "gets the last action which has been added for <JAIL>"],
["get <JAIL> actions", "gets a list of actions for <JAIL>"], ["get <JAIL> actions", "gets a list of actions for <JAIL>"],
["get <JAIL> actionstart <ACT>", "gets the start command for the action <ACT> for <JAIL>"], ["", "COMMAND ACTION INFORMATION",""],
["get <JAIL> actionstop <ACT>", "gets the stop command for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> actionstart", "gets the start command for the action <ACT> for <JAIL>"],
["get <JAIL> actioncheck <ACT>", "gets the check command for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> actionstop", "gets the stop command for the action <ACT> for <JAIL>"],
["get <JAIL> actionban <ACT>", "gets the ban command for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> actioncheck", "gets the check command for the action <ACT> for <JAIL>"],
["get <JAIL> actionunban <ACT>", "gets the unban command for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> actionban", "gets the ban command for the action <ACT> for <JAIL>"],
["get <JAIL> cinfo <ACT> <KEY>", "gets the value for <KEY> for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> actionunban", "gets the unban command for the action <ACT> for <JAIL>"],
["get <JAIL> timeout <ACT>", "gets the command timeout in seconds for the action <ACT> for <JAIL>"], ["get <JAIL> action <ACT> timeout", "gets the command timeout in seconds for the action <ACT> for <JAIL>"],
["", "GENERAL ACTION INFORMATION", ""],
["get <JAIL> actionproperties <ACT>", "gets a list of properties for the action <ACT> for <JAIL>"],
["get <JAIL> actionmethods <ACT>", "gets a list of methods for the action <ACT> for <JAIL>"],
["get <JAIL> action <ACT> <PROPERTY>", "gets the value of <PROPERTY> for the action <ACT> for <JAIL>"],
] ]
## ##
@ -125,12 +130,14 @@ def printFormatted():
print print
firstHeading = True firstHeading = True
first = True first = True
for n in textwrap.wrap(m[1], WIDTH): if len(m[0]) >= MARGIN:
m[1] = ' ' * WIDTH + m[1]
for n in textwrap.wrap(m[1], WIDTH, drop_whitespace=False):
if first: if first:
line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n line = ' ' * INDENT + m[0] + ' ' * (MARGIN - len(m[0])) + n.strip()
first = False first = False
else: else:
line = ' ' * (INDENT + MARGIN) + n line = ' ' * (INDENT + MARGIN) + n.strip()
print line print line
## ##

View File

@ -23,6 +23,8 @@ __license__ = "GPL"
import logging, os, subprocess, time, signal, tempfile import logging, os, subprocess, time, signal, tempfile
import threading, re import threading, re
from abc import ABCMeta
from collections import MutableMapping
#from subprocess import call #from subprocess import call
# Gets the instance of the logger. # Gets the instance of the logger.
@ -46,232 +48,326 @@ _RETCODE_HINTS = {
signame = dict((num, name) signame = dict((num, name)
for name, num in signal.__dict__.iteritems() if name.startswith("SIG")) for name, num in signal.__dict__.iteritems() if name.startswith("SIG"))
## class CallingMap(MutableMapping):
# Execute commands. """A Mapping type which returns the result of callable values.
#
# This class reads the failures from the Jail queue and decide if an
# action has to be taken. A BanManager take care of the banned IP
# addresses.
class Action: `CallingMap` behaves similar to a standard python dictionary,
with the exception that any values which are callable, are called
and the result is returned as the value.
No error handling is in place, such that any errors raised in the
callable will raised as usual.
Actual dictionary is stored in property `data`, and can be accessed
to obtain original callable values.
def __init__(self, name): Attributes
self.__name = name ----------
self.__timeout = 60 data : dict
self.__cInfo = dict() The dictionary data which can be accessed to obtain items
## Command executed in order to initialize the system. without callable values being called.
self.__actionStart = ''
## Command executed when an IP address gets banned.
self.__actionBan = ''
## Command executed when an IP address gets removed.
self.__actionUnban = ''
## Command executed in order to check requirements.
self.__actionCheck = ''
## Command executed in order to stop the system.
self.__actionStop = ''
logSys.debug("Created Action")
## """
# Sets the action name.
#
# @param name the name of the action
def setName(self, name): def __init__(self, *args, **kwargs):
self.__name = name self.data = dict(*args, **kwargs)
## def __getitem__(self, key):
# Returns the action name. value = self.data[key]
# if callable(value):
# @return the name of the action return value()
else:
return value
def getName(self): def __setitem__(self, key, value):
return self.__name self.data[key] = value
## def __delitem__(self, key):
# Sets the timeout period for commands. del self.data[key]
#
# @param timeout timeout period in seconds
def setTimeout(self, timeout): def __iter__(self):
self.__timeout = int(timeout) return iter(self.data)
logSys.debug("Set action %s timeout = %i" % (self.__name, timeout))
## def __len__(self):
# Returns the action timeout period for commands. return len(self.data)
#
# @return the timeout period in seconds
def getTimeout(self): class ActionBase(object):
return self.__timeout """An abstract base class for actions in Fail2Ban.
## Action Base is a base definition of what methods need to be in
# Sets a "CInfo". place to create a Python based action for Fail2Ban. This class can
# be inherited from to ease implementation.
# CInfo are statically defined properties. They can be definied by Required methods:
# the user and are used to set e-mail addresses, port, host or - __init__(jail, name)
# anything that should not change during the life of the server. - start()
# - stop()
# @param key the property name - ban(aInfo)
# @param value the property value - unban(aInfo)
"""
__metaclass__ = ABCMeta
def setCInfo(self, key, value): @classmethod
self.__cInfo[key] = value def __subclasshook__(cls, C):
required = (
## "start",
# Returns a "CInfo". "stop",
# "ban",
# @param key the property name "unban",
)
def getCInfo(self, key): for method in required:
return self.__cInfo[key] if not callable(getattr(C, method, None)):
##
# Removes a "CInfo".
#
# @param key the property name
def delCInfo(self, key):
del self.__cInfo[key]
##
# Set the "start" command.
#
# @param value the command
def setActionStart(self, value):
self.__actionStart = value
logSys.debug("Set actionStart = %s" % value)
##
# Get the "start" command.
#
# @return the command
def getActionStart(self):
return self.__actionStart
##
# Executes the action "start" command.
#
# Replaces the tags in the action command with value of "cInfo"
# and executes the resulting command.
#
# @return True if the command succeeded
def execActionStart(self):
if self.__cInfo:
if not Action.substituteRecursiveTags(self.__cInfo):
logSys.error("Cinfo/definitions contain self referencing definitions and cannot be resolved")
return False return False
startCmd = Action.replaceTag(self.__actionStart, self.__cInfo) return True
return Action.executeCmd(startCmd, self.__timeout)
## def __init__(self, jail, name):
# Set the "ban" command. """Initialise action.
#
# @param value the command
def setActionBan(self, value): Called when action is created, but before the jail/actions is
self.__actionBan = value started. This should carry out necessary methods to initialise
logSys.debug("Set actionBan = %s" % value) the action but not "start" the action.
## Parameters
# Get the "ban" command. ----------
# jail : Jail
# @return the command The jail in which the action belongs to.
name : str
Name assigned to the action.
def getActionBan(self): Notes
return self.__actionBan -----
Any additional arguments specified in `jail.conf` or passed
via `fail2ban-client` will be passed as keyword arguments.
"""
self._jail = jail
self._name = name
self._logSys = logging.getLogger(
'%s.%s' % (__name__, self.__class__.__name__))
## def start(self):
# Executes the action "ban" command. """Executed when the jail/action is started.
# """
# @return True if the command succeeded pass
def execActionBan(self, aInfo): def stop(self):
return self.__processCmd(self.__actionBan, aInfo) """Executed when the jail/action is stopped.
"""
pass
## def ban(self, aInfo):
# Set the "unban" command. """Executed when a ban occurs.
#
# @param value the command
def setActionUnban(self, value): Parameters
self.__actionUnban = value ----------
logSys.debug("Set actionUnban = %s" % value) aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
pass
## def unban(self, aInfo):
# Get the "unban" command. """Executed when a ban expires.
#
# @return the command
def getActionUnban(self): Parameters
return self.__actionUnban ----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
pass
## class CommandAction(ActionBase):
# Executes the action "unban" command. """A action which executes OS shell commands.
#
# @return True if the command succeeded
def execActionUnban(self, aInfo): This is the default type of action which Fail2Ban uses.
return self.__processCmd(self.__actionUnban, aInfo) """
## def __init__(self, jail, name):
# Set the "check" command. """Initialise action.
#
# @param value the command
def setActionCheck(self, value): Default sets all commands for actions as empty string, such
self.__actionCheck = value no command is executed.
logSys.debug("Set actionCheck = %s" % value)
## Parameters
# Get the "check" command. ----------
# jail : Jail
# @return the command The jail in which the action belongs to.
name : str
Name assigned to the action.
"""
def getActionCheck(self): super(CommandAction, self).__init__(jail, name)
return self.__actionCheck self.timeout = 60
## Command executed in order to initialize the system.
self.actionstart = ''
## Command executed when an IP address gets banned.
self.actionban = ''
## Command executed when an IP address gets removed.
self.actionunban = ''
## Command executed in order to check requirements.
self.actioncheck = ''
## Command executed in order to stop the system.
self.actionstop = ''
self._logSys.debug("Created %s" % self.__class__)
## @classmethod
# Set the "stop" command. def __subclasshook__(cls, C):
# return NotImplemented # Standard checks
# @param value the command
def setActionStop(self, value): @property
self.__actionStop = value def timeout(self):
logSys.debug("Set actionStop = %s" % value) """Time out period in seconds for execution of commands.
"""
return self._timeout
## @timeout.setter
# Get the "stop" command. def timeout(self, timeout):
# self._timeout = int(timeout)
# @return the command self._logSys.debug("Set action %s timeout = %i" %
(self._name, self.timeout))
def getActionStop(self): @property
return self.__actionStop def _properties(self):
"""A dictionary of the actions properties.
## This is used to subsitute "tags" in the commands.
# Executes the action "stop" command. """
# return dict(
# Replaces the tags in the action command with value of "cInfo" (key, getattr(self, key))
# and executes the resulting command. for key in dir(self)
# if not key.startswith("_") and not callable(getattr(self, key)))
# @return True if the command succeeded
def execActionStop(self): @property
stopCmd = Action.replaceTag(self.__actionStop, self.__cInfo) def actionstart(self):
return Action.executeCmd(stopCmd, self.__timeout) """The command executed on start of the jail/action.
"""
return self._actionstart
## @actionstart.setter
# Sort out tag definitions within other tags def actionstart(self, value):
# self._actionstart = value
# so: becomes: self._logSys.debug("Set actionstart = %s" % value)
# a = 3 a = 3
# b = <a>_3 b = 3_3 def start(self):
# @param tags, a dictionary """Executes the "actionstart" command.
# @returns tags altered or False if there is a recursive definition
#@staticmethod Replace the tags in the action command with actions properties
and executes the resulting command.
"""
if (self._properties and
not self.substituteRecursiveTags(self._properties)):
self._logSys.error(
"properties contain self referencing definitions "
"and cannot be resolved")
raise RuntimeError("Error starting action")
startCmd = self.replaceTag(self.actionstart, self._properties)
if not self.executeCmd(startCmd, self.timeout):
raise RuntimeError("Error starting action")
@property
def actionban(self):
"""The command used when a ban occurs.
"""
return self._actionban
@actionban.setter
def actionban(self, value):
self._actionban = value
self._logSys.debug("Set actionban = %s" % value)
def ban(self, aInfo):
"""Executes the "actionban" command.
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
if not self._processCmd(self.actionban, aInfo):
raise RuntimeError("Error banning %(ip)s" % aInfo)
@property
def actionunban(self):
"""The command used when an unban occurs.
"""
return self._actionunban
@actionunban.setter
def actionunban(self, value):
self._actionunban = value
self._logSys.debug("Set actionunban = %s" % value)
def unban(self, aInfo):
"""Executes the "actionunban" command.
Replaces the tags in the action command with actions properties
and ban information, and executes the resulting command.
Parameters
----------
aInfo : dict
Dictionary which includes information in relation to
the ban.
"""
if not self._processCmd(self.actionunban, aInfo):
raise RuntimeError("Error unbanning %(ip)s" % aInfo)
@property
def actioncheck(self):
"""The command used to check the environment.
This is used prior to a ban taking place to ensure the
environment is appropriate. If this check fails, `stop` and
`start` is executed prior to the check being called again.
"""
return self._actioncheck
@actioncheck.setter
def actioncheck(self, value):
self._actioncheck = value
self._logSys.debug("Set actioncheck = %s" % value)
@property
def actionstop(self):
"""The command executed when the jail/actions stops.
"""
return self._actionstop
@actionstop.setter
def actionstop(self, value):
self._actionstop = value
self._logSys.debug("Set actionstop = %s" % value)
def stop(self):
"""Executes the "actionstop" command.
Replaces the tags in the action command with actions properties
and executes the resulting command.
"""
stopCmd = self.replaceTag(self.actionstop, self._properties)
if not self.executeCmd(stopCmd, self.timeout):
raise RuntimeError("Error stopping action")
@staticmethod
def substituteRecursiveTags(tags): def substituteRecursiveTags(tags):
"""Sort out tag definitions within other tags.
so: becomes:
a = 3 a = 3
b = <a>_3 b = 3_3
Parameters
----------
tags : dict
Dictionary of tags(keys) and their values.
Returns
-------
dict
Dictionary of tags(keys) and their values, with tags
within the values recursively replaced.
"""
t = re.compile(r'<([^ >]+)>') t = re.compile(r'<([^ >]+)>')
for tag, value in tags.iteritems(): for tag, value in tags.iteritems():
value = str(value) value = str(value)
@ -299,98 +395,130 @@ class Action:
#logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value)) #logSys.log(5, 'TAG: %s, newvalue: %s' % (tag, value))
tags[tag] = value tags[tag] = value
return tags return tags
substituteRecursiveTags = staticmethod(substituteRecursiveTags)
#@staticmethod @staticmethod
def escapeTag(tag): def escapeTag(value):
"""Escape characters which may be used for command injection.
Parameters
----------
value : str
A string of which characters will be escaped.
Returns
-------
str
`value` with certain characters escaped.
Notes
-----
The following characters are escaped::
\\#&;`|*?~<>^()[]{}$'"
"""
for c in '\\#&;`|*?~<>^()[]{}$\'"': for c in '\\#&;`|*?~<>^()[]{}$\'"':
if c in tag: if c in value:
tag = tag.replace(c, '\\' + c) value = value.replace(c, '\\' + c)
return tag return value
escapeTag = staticmethod(escapeTag)
## @classmethod
# Replaces tags in query with property values in aInfo. def replaceTag(cls, query, aInfo):
# """Replaces tags in `query` with property values.
# @param query the query string with tags
# @param aInfo the properties
# @return a string
#@staticmethod Parameters
def replaceTag(query, aInfo): ----------
""" Replace tags in query query : str
String with tags.
aInfo : dict
Tags(keys) and associated values for substitution in query.
Returns
-------
str
`query` string with tags replaced.
""" """
string = query string = query
for tag, value in aInfo.iteritems(): for tag in aInfo:
if "<%s>" % tag in query: if "<%s>" % tag in query:
if callable(value): value = str(aInfo[tag]) # assure string
value = value()
value = str(value) # assure string
if tag.endswith('matches'): if tag.endswith('matches'):
# That one needs to be escaped since its content is # That one needs to be escaped since its content is
# out of our control # out of our control
value = Action.escapeTag(value) value = cls.escapeTag(value)
string = string.replace('<' + tag + '>', value) string = string.replace('<' + tag + '>', value)
# New line # New line
string = string.replace("<br>", '\n') string = string.replace("<br>", '\n')
return string return string
replaceTag = staticmethod(replaceTag)
## def _processCmd(self, cmd, aInfo = None):
# Executes a command with preliminary checks and substitutions. """Executes a command with preliminary checks and substitutions.
#
# Before executing any commands, executes the "check" command first
# in order to check if pre-requirements are met. If this check fails,
# it tries to restore a sane environment before executing the real
# command.
# Replaces "aInfo" and "cInfo" in the query too.
#
# @param cmd The command to execute
# @param aInfo Dynamic properties
# @return True if the command succeeded
def __processCmd(self, cmd, aInfo = None): Before executing any commands, executes the "check" command first
""" Executes an OS command. in order to check if pre-requirements are met. If this check fails,
it tries to restore a sane environment before executing the real
command.
Parameters
----------
cmd : str
The command to execute.
aInfo : dictionary
Dynamic properties.
Returns
-------
bool
True if the command succeeded.
""" """
if cmd == "": if cmd == "":
logSys.debug("Nothing to do") self._logSys.debug("Nothing to do")
return True return True
checkCmd = Action.replaceTag(self.__actionCheck, self.__cInfo) checkCmd = self.replaceTag(self.actioncheck, self._properties)
if not Action.executeCmd(checkCmd, self.__timeout): if not self.executeCmd(checkCmd, self.timeout):
logSys.error("Invariant check failed. Trying to restore a sane" + self._logSys.error(
" environment") "Invariant check failed. Trying to restore a sane environment")
self.execActionStop() self.stop()
self.execActionStart() self.start()
if not Action.executeCmd(checkCmd, self.__timeout): if not self.executeCmd(checkCmd, self.timeout):
logSys.fatal("Unable to restore environment") self._logSys.fatal("Unable to restore environment")
return False return False
# Replace tags # Replace tags
if not aInfo is None: if not aInfo is None:
realCmd = Action.replaceTag(cmd, aInfo) realCmd = self.replaceTag(cmd, aInfo)
else: else:
realCmd = cmd realCmd = cmd
# Replace static fields # Replace static fields
realCmd = Action.replaceTag(realCmd, self.__cInfo) realCmd = self.replaceTag(realCmd, self._properties)
return Action.executeCmd(realCmd, self.__timeout) return self.executeCmd(realCmd, self.timeout)
## @staticmethod
# Executes a command.
#
# We need a shell here because commands are mainly shell script. They
# contain pipe, redirection, etc.
#
# @todo Force the use of bash!?
# @todo Kill the command after a given timeout
#
# @param realCmd the command to execute
# @return True if the command succeeded
#@staticmethod
def executeCmd(realCmd, timeout=60): def executeCmd(realCmd, timeout=60):
"""Executes a command.
Parameters
----------
realCmd : str
The command to execute.
timeout : int
The time out in seconds for the command.
Returns
-------
bool
True if the command succeeded.
Raises
------
OSError
If command fails to be executed.
RuntimeError
If command execution times out.
"""
logSys.debug(realCmd) logSys.debug(realCmd)
if not realCmd: if not realCmd:
logSys.debug("Nothing to do") logSys.debug("Nothing to do")
@ -420,7 +548,6 @@ class Action:
retcode = popen.poll() retcode = popen.poll()
except OSError, e: except OSError, e:
logSys.error("%s -- failed with %s" % (realCmd, e)) logSys.error("%s -- failed with %s" % (realCmd, e))
return False
finally: finally:
_cmd_lock.release() _cmd_lock.release()
@ -448,5 +575,5 @@ class Action:
logSys.info("HINT on %i: %s" logSys.info("HINT on %i: %s"
% (retcode, msg % locals())) % (retcode, msg % locals()))
return False return False
executeCmd = staticmethod(executeCmd) raise RuntimeError("Command execution failed: %s" % realCmd)

View File

@ -24,93 +24,112 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from banmanager import BanManager
from jailthread import JailThread
from action import Action
from mytime import MyTime
import time, logging import time, logging
import os
import imp
from collections import Mapping
from .banmanager import BanManager
from .jailthread import JailThread
from .action import ActionBase, CommandAction, CallingMap
from .mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
## class Actions(JailThread, Mapping):
# Execute commands. """Handles jail actions.
#
# This class reads the failures from the Jail queue and decide if an
# action has to be taken. A BanManager take care of the banned IP
# addresses.
class Actions(JailThread): This class handles the actions of the jail. Creation, deletion or to
actions must be done through this class. This class is based on the
## Mapping type, and the `add` method must be used to add new actions.
# Constructor. This class also starts and stops the actions, and fetches bans from
# the jail executing these bans via the actions.
# Initialize the filter object with default values. """
# @param jail the jail object
def __init__(self, jail): def __init__(self, jail):
"""Initialise an empty Actions instance.
Parameters
----------
jail: Jail
The jail of which the actions belongs to.
"""
JailThread.__init__(self) JailThread.__init__(self)
## The jail which contains this action. ## The jail which contains this action.
self.jail = jail self._jail = jail
self.__actions = list() self._actions = dict()
## The ban manager. ## The ban manager.
self.__banManager = BanManager() self.__banManager = BanManager()
## def add(self, name, pythonModule=None, initOpts=None):
# Adds an action. """Adds a new action.
#
# @param name The action name
def addAction(self, name): Add a new action if not already present, defaulting to standard
`CommandAction`, or specified Python module.
Parameters
----------
name : str
The name of the action.
pythonModule : str, optional
Path to Python file which must contain `Action` class.
Default None, which means `CommandAction` is used.
initOpts : dict, optional
Options for Python Action, used as keyword arguments for
initialisation. Default None.
Raises
------
ValueError
If action name already exists.
RuntimeError
If external Python module does not have `Action` class
or does not implement necessary methods as per `ActionBase`
abstract class.
"""
# Check is action name already exists # Check is action name already exists
if name in [action.getName() for action in self.__actions]: if name in self._actions:
raise ValueError("Action %s already exists" % name) raise ValueError("Action %s already exists" % name)
action = Action(name) if pythonModule is None:
self.__actions.append(action) action = CommandAction(self._jail, name)
else:
pythonModuleName = os.path.basename(pythonModule.strip(".py"))
customActionModule = imp.load_source(
pythonModuleName, pythonModule)
if not hasattr(customActionModule, "Action"):
raise RuntimeError(
"%s module does not have 'Action' class" % pythonModule)
elif not issubclass(customActionModule.Action, ActionBase):
raise RuntimeError(
"%s module %s does not implement required methods" % (
pythonModule, customActionModule.Action.__name__))
action = customActionModule.Action(self._jail, name, **initOpts)
self._actions[name] = action
## def __getitem__(self, name):
# Removes an action. try:
# return self._actions[name]
# @param name The action name except KeyError:
def delAction(self, name):
for action in self.__actions:
if action.getName() == name:
self.__actions.remove(action)
return
raise KeyError("Invalid Action name: %s" % name) raise KeyError("Invalid Action name: %s" % name)
## def __delitem__(self, name):
# Returns an action. try:
# del self._actions[name]
# Raises a KeyError exception if the action does not exist. except KeyError:
# raise KeyError("Invalid Action name: %s" % name)
# @param name the action name
# @return the action
def getAction(self, name): def __iter__(self):
for action in self.__actions: return iter(self._actions)
if action.getName() == name:
return action
raise KeyError("Invalid Action name")
## def __len__(self):
# Returns the last defined action. return len(self._actions)
#
# @return The last defined action.
def getLastAction(self): def __eq__(self, other): # Required for Threading
action = self.__actions.pop() return False
self.__actions.append(action)
return action
## def __hash__(self): # Required for Threading
# Returns the list of actions return id(self)
#
# @return list of actions
def getActions(self):
return self.__actions
## ##
# Set the ban time. # Set the ban time.
@ -129,33 +148,51 @@ class Actions(JailThread):
def getBanTime(self): def getBanTime(self):
return self.__banManager.getBanTime() return self.__banManager.getBanTime()
##
# Remove a banned IP now, rather than waiting for it to expire, even if set to never expire.
#
# @return the IP string or 'None' if not unbanned.
def removeBannedIP(self, ip): def removeBannedIP(self, ip):
"""Removes banned IP calling actions' unban method
Remove a banned IP now, rather than waiting for it to expire,
even if set to never expire.
Parameters
----------
ip : str
The IP address to unban
Raises
------
ValueError
If `ip` is not banned
"""
# Find the ticket with the IP. # Find the ticket with the IP.
ticket = self.__banManager.getTicketByIP(ip) ticket = self.__banManager.getTicketByIP(ip)
if ticket is not None: if ticket is not None:
# Unban the IP. # Unban the IP.
self.__unBan(ticket) self.__unBan(ticket)
return ip else:
raise ValueError("IP %s is not banned" % ip) raise ValueError("IP %s is not banned" % ip)
##
# Main loop.
#
# This function is the main loop of the thread. It checks the Jail
# queue and executes commands when an IP address is banned.
# @return True when the thread exits nicely
def run(self): def run(self):
"""Main loop for Threading.
This function is the main loop of the thread. It checks the jail
queue and executes commands when an IP address is banned.
Returns
-------
bool
True when the thread exits nicely.
"""
self.setActive(True) self.setActive(True)
for action in self.__actions: for name, action in self._actions.iteritems():
action.execActionStart() try:
action.start()
except Exception as e:
logSys.error("Failed to start jail '%s' action '%s': %s",
self._jail.getName(), name, e)
while self._isActive(): while self._isActive():
if not self.getIdle(): if not self.getIdle():
#logSys.debug(self.jail.getName() + ": action") #logSys.debug(self._jail.getName() + ": action")
ret = self.__checkBan() ret = self.__checkBan()
if not ret: if not ret:
self.__checkUnBan() self.__checkUnBan()
@ -163,94 +200,116 @@ class Actions(JailThread):
else: else:
time.sleep(self.getSleepTime()) time.sleep(self.getSleepTime())
self.__flushBan() self.__flushBan()
for action in self.__actions: for name, action in self._actions.iteritems():
action.execActionStop() try:
logSys.debug(self.jail.getName() + ": action terminated") action.stop()
except Exception as e:
logSys.error("Failed to stop jail '%s' action '%s': %s",
self._jail.getName(), name, e)
logSys.debug(self._jail.getName() + ": action terminated")
return True return True
##
# Check for IP address to ban.
#
# Look in the Jail queue for FailTicket. If a ticket is available,
# it executes the "ban" command and add a ticket to the BanManager.
# @return True if an IP address get banned
def __checkBan(self): def __checkBan(self):
ticket = self.jail.getFailTicket() """Check for IP address to ban.
Look in the jail queue for FailTicket. If a ticket is available,
it executes the "ban" command and adds a ticket to the BanManager.
Returns
-------
bool
True if an IP address get banned.
"""
ticket = self._jail.getFailTicket()
if ticket != False: if ticket != False:
aInfo = dict() aInfo = CallingMap()
bTicket = BanManager.createBanTicket(ticket) bTicket = BanManager.createBanTicket(ticket)
aInfo["ip"] = bTicket.getIP() aInfo["ip"] = bTicket.getIP()
aInfo["failures"] = bTicket.getAttempt() aInfo["failures"] = bTicket.getAttempt()
aInfo["time"] = bTicket.getTime() aInfo["time"] = bTicket.getTime()
aInfo["matches"] = "\n".join(bTicket.getMatches()) aInfo["matches"] = "\n".join(bTicket.getMatches())
if self.jail.getDatabase() is not None: if self._jail.getDatabase() is not None:
aInfo["ipmatches"] = lambda: "\n".join( aInfo["ipmatches"] = lambda: "\n".join(
self.jail.getDatabase().getBansMerged( self._jail.getDatabase().getBansMerged(
ip=bTicket.getIP()).getMatches()) ip=bTicket.getIP()).getMatches())
aInfo["ipjailmatches"] = lambda: "\n".join( aInfo["ipjailmatches"] = lambda: "\n".join(
self.jail.getDatabase().getBansMerged( self._jail.getDatabase().getBansMerged(
ip=bTicket.getIP(), jail=self.jail).getMatches()) ip=bTicket.getIP(), jail=self._jail).getMatches())
aInfo["ipfailures"] = lambda: "\n".join( aInfo["ipfailures"] = lambda: "\n".join(
self.jail.getDatabase().getBansMerged( self._jail.getDatabase().getBansMerged(
ip=bTicket.getIP()).getAttempt()) ip=bTicket.getIP()).getAttempt())
aInfo["ipjailfailures"] = lambda: "\n".join( aInfo["ipjailfailures"] = lambda: "\n".join(
self.jail.getDatabase().getBansMerged( self._jail.getDatabase().getBansMerged(
ip=bTicket.getIP(), jail=self.jail).getAttempt()) ip=bTicket.getIP(), jail=self._jail).getAttempt())
if self.__banManager.addBanTicket(bTicket): if self.__banManager.addBanTicket(bTicket):
logSys.warning("[%s] Ban %s" % (self.jail.getName(), aInfo["ip"])) logSys.warning("[%s] Ban %s" % (self._jail.getName(), aInfo["ip"]))
for action in self.__actions: for name, action in self._actions.iteritems():
action.execActionBan(aInfo) try:
action.ban(aInfo)
except Exception as e:
logSys.error(
"Failed to execute ban jail '%s' action '%s': %s",
self._jail.getName(), name, e)
return True return True
else: else:
logSys.info("[%s] %s already banned" % (self.jail.getName(), logSys.info("[%s] %s already banned" % (self._jail.getName(),
aInfo["ip"])) aInfo["ip"]))
return False return False
##
# Check for IP address to unban.
#
# Unban IP address which are outdated.
def __checkUnBan(self): def __checkUnBan(self):
"""Check for IP address to unban.
Unban IP addresses which are outdated.
"""
for ticket in self.__banManager.unBanList(MyTime.time()): for ticket in self.__banManager.unBanList(MyTime.time()):
self.__unBan(ticket) self.__unBan(ticket)
##
# Flush the ban list.
#
# Unban all IP address which are still in the banning list.
def __flushBan(self): def __flushBan(self):
"""Flush the ban list.
Unban all IP address which are still in the banning list.
"""
logSys.debug("Flush ban list") logSys.debug("Flush ban list")
for ticket in self.__banManager.flushBanList(): for ticket in self.__banManager.flushBanList():
self.__unBan(ticket) self.__unBan(ticket)
##
# Unbans host corresponding to the ticket.
#
# Executes the actions in order to unban the host given in the
# ticket.
def __unBan(self, ticket): def __unBan(self, ticket):
"""Unbans host corresponding to the ticket.
Executes the actions in order to unban the host given in the
ticket.
Parameters
----------
ticket : FailTicket
Ticket of failures of which to unban
"""
aInfo = dict() aInfo = dict()
aInfo["ip"] = ticket.getIP() aInfo["ip"] = ticket.getIP()
aInfo["failures"] = ticket.getAttempt() aInfo["failures"] = ticket.getAttempt()
aInfo["time"] = ticket.getTime() aInfo["time"] = ticket.getTime()
aInfo["matches"] = "".join(ticket.getMatches()) aInfo["matches"] = "".join(ticket.getMatches())
logSys.warning("[%s] Unban %s" % (self.jail.getName(), aInfo["ip"])) logSys.warning("[%s] Unban %s" % (self._jail.getName(), aInfo["ip"]))
for action in self.__actions: for name, action in self._actions.iteritems():
action.execActionUnban(aInfo) try:
action.unban(aInfo)
except Exception as e:
## logSys.error(
# Get the status of the filter. "Failed to execute unban jail '%s' action '%s': %s",
# self._jail.getName(), name, e)
# Get some informations about the filter state such as the total
# number of failures.
# @return a list with tuple
def status(self): def status(self):
"""Get the status of the filter.
Get some informations about the filter state such as the total
number of failures.
Returns
-------
list
List of tuple pairs, each containing a description and value
for general status information.
"""
ret = [("Currently banned", self.__banManager.size()), ret = [("Currently banned", self.__banManager.size()),
("Total banned", self.__banManager.getBanTotal()), ("Total banned", self.__banManager.getBanTotal()),
("IP list", self.__banManager.getBanList())] ("IP list", self.__banManager.getBanList())]

View File

@ -27,7 +27,7 @@ __license__ = "GPL"
from pickle import dumps, loads, HIGHEST_PROTOCOL from pickle import dumps, loads, HIGHEST_PROTOCOL
import asyncore, asynchat, socket, os, logging, sys, traceback, fcntl import asyncore, asynchat, socket, os, logging, sys, traceback, fcntl
from fail2ban import helpers from .. import helpers
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -76,7 +76,7 @@ class RequestHandler(asynchat.async_chat):
# Serializes the response. # Serializes the response.
message = dumps(message, HIGHEST_PROTOCOL) message = dumps(message, HIGHEST_PROTOCOL)
# Sends the response to the client. # Sends the response to the client.
self.send(message + RequestHandler.END_STRING) self.push(message + RequestHandler.END_STRING)
# Closes the channel. # Closes the channel.
self.close_when_done() self.close_when_done()

View File

@ -24,10 +24,11 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from ticket import BanTicket
from threading import Lock
from mytime import MyTime
import logging import logging
from threading import Lock
from .ticket import BanTicket
from .mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -29,8 +29,8 @@ import json
import locale import locale
from functools import wraps from functools import wraps
from fail2ban.server.mytime import MyTime from .mytime import MyTime
from fail2ban.server.ticket import FailTicket from .ticket import FailTicket
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -22,10 +22,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import sys, time, logging import sys, time, logging
from datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601
from threading import Lock from threading import Lock
from .datetemplate import DatePatternRegex, DateTai64n, DateEpoch, DateISO8601
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -25,14 +25,13 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import re, time, calendar import re, time, calendar
import logging
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from mytime import MyTime from .mytime import MyTime
import iso8601 from . import iso8601
import logging
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,11 +24,12 @@ __author__ = "Cyril Jaquier"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier" __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from faildata import FailData
from ticket import FailTicket
from threading import Lock from threading import Lock
import logging import logging
from .faildata import FailData
from .ticket import FailTicket
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -21,18 +21,17 @@ __author__ = "Cyril Jaquier and Fail2Ban Contributors"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2013 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty
from failmanager import FailManager
from ticket import FailTicket
from jailthread import JailThread
from datedetector import DateDetector
from datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n
from mytime import MyTime
from failregex import FailRegex, Regex, RegexException
from action import Action
import logging, re, os, fcntl, time, sys, locale, codecs import logging, re, os, fcntl, time, sys, locale, codecs
from .failmanager import FailManagerEmpty, FailManager
from .ticket import FailTicket
from .jailthread import JailThread
from .datedetector import DateDetector
from .datetemplate import DatePatternRegex, DateISO8601, DateEpoch, DateTai64n
from .mytime import MyTime
from .failregex import FailRegex, Regex, RegexException
from .action import CommandAction
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -378,9 +377,9 @@ class Filter(JailThread):
return True return True
if self.__ignoreCommand: if self.__ignoreCommand:
command = Action.replaceTag(self.__ignoreCommand, { 'ip': ip } ) command = CommandAction.replaceTag(self.__ignoreCommand, { 'ip': ip } )
logSys.debug('ignore command: ' + command) logSys.debug('ignore command: ' + command)
return Action.executeCmd(command) return CommandAction.executeCmd(command)
return False return False

View File

@ -23,11 +23,13 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty import time, logging, fcntl
from filter import FileFilter
from mytime import MyTime
import time, logging, gamin, fcntl import gamin
from .failmanager import FailManagerEmpty
from .filter import FileFilter
from .mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,12 +24,12 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier; 2012 Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from failmanager import FailManagerEmpty
from filter import FileFilter
from mytime import MyTime
import time, logging, os import time, logging, os
from .failmanager import FailManagerEmpty
from .filter import FileFilter
from .mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)

View File

@ -24,13 +24,12 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2011-2012 Lee Clemens, 2012 Y
__license__ = "GPL" __license__ = "GPL"
import time, logging, pyinotify import time, logging, pyinotify
from distutils.version import LooseVersion from distutils.version import LooseVersion
from os.path import dirname, sep as pathsep from os.path import dirname, sep as pathsep
from failmanager import FailManagerEmpty from .failmanager import FailManagerEmpty
from filter import FileFilter from .filter import FileFilter
from mytime import MyTime from .mytime import MyTime
if not hasattr(pyinotify, '__version__') \ if not hasattr(pyinotify, '__version__') \

View File

@ -29,9 +29,9 @@ from systemd import journal
if LooseVersion(getattr(journal, '__version__', "0")) < '204': if LooseVersion(getattr(journal, '__version__', "0")) < '204':
raise ImportError("Fail2Ban requires systemd >= 204") raise ImportError("Fail2Ban requires systemd >= 204")
from failmanager import FailManagerEmpty from .failmanager import FailManagerEmpty
from filter import JournalFilter from .filter import JournalFilter
from mytime import MyTime from .mytime import MyTime
# Gets the instance of the logger. # Gets the instance of the logger.

View File

@ -25,7 +25,7 @@ __license__ = "GPL"
import Queue, logging import Queue, logging
from actions import Actions from .actions import Actions
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -71,7 +71,7 @@ class Jail:
"%r was requested" % (b, backend)) "%r was requested" % (b, backend))
else: else:
logSys.info("Initiated %r backend" % b) logSys.info("Initiated %r backend" % b)
self.__action = Actions(self) self.__actions = Actions(self)
return # we are done return # we are done
except ImportError, e: except ImportError, e:
logSys.debug( logSys.debug(
@ -110,11 +110,10 @@ class Jail:
self.__filter = FilterSystemd(self) self.__filter = FilterSystemd(self)
def setName(self, name): def setName(self, name):
# 20 based on iptable chain name limit of 30 less len('fail2ban-') # 26 based on iptable chain name limit of 30 less len('f2b-')
if len(name) >= 20: if len(name) >= 26:
logSys.warning("Jail name %r might be too long and some commands" logSys.warning("Jail name %r might be too long and some commands "
" (e.g. iptables) might not function correctly." "might not function correctly. Please shorten"
" Please shorten"
% name) % name)
self.__name = name self.__name = name
@ -124,11 +123,17 @@ class Jail:
def getDatabase(self): def getDatabase(self):
return self.__db return self.__db
def getFilter(self): @property
def filter(self):
"""The filter which the jail is using to monitor log files.
"""
return self.__filter return self.__filter
def getAction(self): @property
return self.__action def actions(self):
"""Actions object used to manage actions for jail.
"""
return self.__actions
def putFailTicket(self, ticket): def putFailTicket(self, ticket):
self.__queue.put(ticket) self.__queue.put(ticket)
@ -143,36 +148,36 @@ class Jail:
def start(self): def start(self):
self.__filter.start() self.__filter.start()
self.__action.start() self.__actions.start()
# Restore any previous valid bans from the database # Restore any previous valid bans from the database
if self.__db is not None: if self.__db is not None:
for ticket in self.__db.getBans( for ticket in self.__db.getBans(
jail=self, bantime=self.__action.getBanTime()): jail=self, bantime=self.__actions.getBanTime()):
self.__queue.put(ticket) self.__queue.put(ticket)
logSys.info("Jail '%s' started" % self.__name) logSys.info("Jail '%s' started" % self.__name)
def stop(self): def stop(self):
self.__filter.stop() self.__filter.stop()
self.__action.stop() self.__actions.stop()
self.__filter.join() self.__filter.join()
self.__action.join() self.__actions.join()
logSys.info("Jail '%s' stopped" % self.__name) logSys.info("Jail '%s' stopped" % self.__name)
def isAlive(self): def isAlive(self):
isAlive0 = self.__filter.isAlive() isAlive0 = self.__filter.isAlive()
isAlive1 = self.__action.isAlive() isAlive1 = self.__actions.isAlive()
return isAlive0 or isAlive1 return isAlive0 or isAlive1
def setIdle(self, value): def setIdle(self, value):
self.__filter.setIdle(value) self.__filter.setIdle(value)
self.__action.setIdle(value) self.__actions.setIdle(value)
def getIdle(self): def getIdle(self):
return self.__filter.getIdle() or self.__action.getIdle() return self.__filter.getIdle() or self.__actions.getIdle()
def getStatus(self): def getStatus(self):
fStatus = self.__filter.status() fStatus = self.__filter.status()
aStatus = self.__action.status() aStatus = self.__actions.status()
ret = [("filter", fStatus), ret = [("filter", fStatus),
("action", aStatus)] ("action", aStatus)]
return ret return ret

View File

@ -21,137 +21,84 @@ __author__ = "Cyril Jaquier, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko" __copyright__ = "Copyright (c) 2004 Cyril Jaquier, 2013- Yaroslav Halchenko"
__license__ = "GPL" __license__ = "GPL"
from fail2ban.exceptions import DuplicateJailException, UnknownJailException
from jail import Jail
from threading import Lock from threading import Lock
from collections import Mapping
## from ..exceptions import DuplicateJailException, UnknownJailException
# Handles the jails. from .jail import Jail
#
# This class handles the jails. Creation, deletion or access to a jail must be
# done through this class. This class is thread-safe which is not the case of
# the jail itself, including filter and actions.
class Jails:
## class Jails(Mapping):
# Constructor. """Handles the jails.
This class handles the jails. Creation, deletion or access to a jail
must be done through this class. This class is thread-safe which is
not the case of the jail itself, including filter and actions. This
class is based on Mapping type, and the `add` method must be used to
add additional jails.
"""
def __init__(self): def __init__(self):
"""Initialise an empty Jails instance.
"""
self.__lock = Lock() self.__lock = Lock()
self.__jails = dict() self._jails = dict()
##
# Adds a jail.
#
# Adds a new jail which should use the given backend. Raises a
# <code>DuplicateJailException</code> if the jail is already defined.
# @param name The name of the jail
# @param backend The backend to use
def add(self, name, backend, db=None): def add(self, name, backend, db=None):
"""Adds a jail.
Adds a new jail if not already present which should use the
given backend.
Parameters
----------
name : str
The name of the jail.
backend : str
The backend to use.
Raises
------
DuplicateJailException
If jail name is already present.
"""
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.__jails.has_key(name): if name in self._jails:
raise DuplicateJailException(name) raise DuplicateJailException(name)
else: else:
self.__jails[name] = Jail(name, backend, db) self._jails[name] = Jail(name, backend, db)
finally: finally:
self.__lock.release() self.__lock.release()
## def __getitem__(self, name):
# Removes a jail.
#
# Removes the jail <code>name</code>. Raise an <code>UnknownJailException</code>
# if the jail does not exist.
# @param name The name of the jail
def remove(self, name):
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.__jails.has_key(name): return self._jails[name]
del self.__jails[name] except KeyError:
else:
raise UnknownJailException(name) raise UnknownJailException(name)
finally: finally:
self.__lock.release() self.__lock.release()
## def __delitem__(self, name):
# Returns a jail.
#
# Returns the jail <code>name</code>. Raise an <code>UnknownJailException</code>
# if the jail does not exist.
# @param name The name of the jail
def get(self, name):
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.__jails.has_key(name): del self._jails[name]
jail = self.__jails[name] except KeyError:
return jail
else:
raise UnknownJailException(name) raise UnknownJailException(name)
finally: finally:
self.__lock.release() self.__lock.release()
## def __len__(self):
# Returns an action class instance.
#
# Returns the action object of the jail <code>name</code>. Raise an
# <code>UnknownJailException</code> if the jail does not exist.
# @param name The name of the jail
def getAction(self, name):
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.__jails.has_key(name): return len(self._jails)
action = self.__jails[name].getAction()
return action
else:
raise UnknownJailException(name)
finally: finally:
self.__lock.release() self.__lock.release()
## def __iter__(self):
# Returns a filter class instance.
#
# Returns the filter object of the jail <code>name</code>. Raise an
# <code>UnknownJailException</code> if the jail does not exist.
# @param name The name of the jail
def getFilter(self, name):
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.__jails.has_key(name): return iter(self._jails)
action = self.__jails[name].getFilter()
return action
else:
raise UnknownJailException(name)
finally: finally:
self.__lock.release() self.__lock.release()
##
# Returns the jails.
#
# Returns a copy of the jails list.
def getAll(self):
try:
self.__lock.acquire()
return self.__jails.copy()
finally:
self.__lock.release()
##
# Returns the size of the jails.
#
# Returns the number of jails.
def size(self):
try:
self.__lock.acquire()
return len(self.__jails)
finally:
self.__lock.release()

View File

@ -25,15 +25,16 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
from threading import Lock, RLock from threading import Lock, RLock
from jails import Jails
from filter import FileFilter, JournalFilter
from transmitter import Transmitter
from asyncserver import AsyncServer
from asyncserver import AsyncServerException
from database import Fail2BanDb
from fail2ban import version
import logging, logging.handlers, sys, os, signal import logging, logging.handlers, sys, os, signal
from .jails import Jails
from .filter import FileFilter, JournalFilter
from .transmitter import Transmitter
from .asyncserver import AsyncServer, AsyncServerException
from .database import Fail2BanDb
from .action import CommandAction
from .. import version
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -121,10 +122,10 @@ class Server:
def addJail(self, name, backend): def addJail(self, name, backend):
self.__jails.add(name, backend, self.__db) self.__jails.add(name, backend, self.__db)
if self.__db is not None: if self.__db is not None:
self.__db.addJail(self.__jails.get(name)) self.__db.addJail(self.__jails[name])
def delJail(self, name): def delJail(self, name):
self.__jails.remove(name) del self.__jails[name]
if self.__db is not None: if self.__db is not None:
self.__db.delJailName(name) self.__db.delJailName(name)
@ -132,7 +133,7 @@ class Server:
try: try:
self.__lock.acquire() self.__lock.acquire()
if not self.isAlive(name): if not self.isAlive(name):
self.__jails.get(name).start() self.__jails[name].start()
finally: finally:
self.__lock.release() self.__lock.release()
@ -141,7 +142,7 @@ class Server:
try: try:
self.__lock.acquire() self.__lock.acquire()
if self.isAlive(name): if self.isAlive(name):
self.__jails.get(name).stop() self.__jails[name].stop()
self.delJail(name) self.delJail(name)
finally: finally:
self.__lock.release() self.__lock.release()
@ -150,43 +151,43 @@ class Server:
logSys.info("Stopping all jails") logSys.info("Stopping all jails")
try: try:
self.__lock.acquire() self.__lock.acquire()
for jail in self.__jails.getAll(): for jail in self.__jails.keys():
self.stopJail(jail) self.stopJail(jail)
finally: finally:
self.__lock.release() self.__lock.release()
def isAlive(self, name): def isAlive(self, name):
return self.__jails.get(name).isAlive() return self.__jails[name].isAlive()
def setIdleJail(self, name, value): def setIdleJail(self, name, value):
self.__jails.get(name).setIdle(value) self.__jails[name].setIdle(value)
return True return True
def getIdleJail(self, name): def getIdleJail(self, name):
return self.__jails.get(name).getIdle() return self.__jails[name].getIdle()
# Filter # Filter
def addIgnoreIP(self, name, ip): def addIgnoreIP(self, name, ip):
self.__jails.getFilter(name).addIgnoreIP(ip) self.__jails[name].filter.addIgnoreIP(ip)
def delIgnoreIP(self, name, ip): def delIgnoreIP(self, name, ip):
self.__jails.getFilter(name).delIgnoreIP(ip) self.__jails[name].filter.delIgnoreIP(ip)
def getIgnoreIP(self, name): def getIgnoreIP(self, name):
return self.__jails.getFilter(name).getIgnoreIP() return self.__jails[name].filter.getIgnoreIP()
def addLogPath(self, name, fileName, tail=False): def addLogPath(self, name, fileName, tail=False):
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
filter_.addLogPath(fileName, tail) filter_.addLogPath(fileName, tail)
def delLogPath(self, name, fileName): def delLogPath(self, name, fileName):
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
filter_.delLogPath(fileName) filter_.delLogPath(fileName)
def getLogPath(self, name): def getLogPath(self, name):
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
return [m.getFileName() return [m.getFileName()
for m in filter_.getLogPath()] for m in filter_.getLogPath()]
@ -195,17 +196,17 @@ class Server:
return [] return []
def addJournalMatch(self, name, match): # pragma: systemd no cover def addJournalMatch(self, name, match): # pragma: systemd no cover
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, JournalFilter): if isinstance(filter_, JournalFilter):
filter_.addJournalMatch(match) filter_.addJournalMatch(match)
def delJournalMatch(self, name, match): # pragma: systemd no cover def delJournalMatch(self, name, match): # pragma: systemd no cover
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, JournalFilter): if isinstance(filter_, JournalFilter):
filter_.delJournalMatch(match) filter_.delJournalMatch(match)
def getJournalMatch(self, name): # pragma: systemd no cover def getJournalMatch(self, name): # pragma: systemd no cover
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, JournalFilter): if isinstance(filter_, JournalFilter):
return filter_.getJournalMatch() return filter_.getJournalMatch()
else: else:
@ -213,154 +214,109 @@ class Server:
return [] return []
def setLogEncoding(self, name, encoding): def setLogEncoding(self, name, encoding):
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
filter_.setLogEncoding(encoding) filter_.setLogEncoding(encoding)
def getLogEncoding(self, name): def getLogEncoding(self, name):
filter_ = self.__jails.getFilter(name) filter_ = self.__jails[name].filter
if isinstance(filter_, FileFilter): if isinstance(filter_, FileFilter):
return filter_.getLogEncoding() return filter_.getLogEncoding()
def setFindTime(self, name, value): def setFindTime(self, name, value):
self.__jails.getFilter(name).setFindTime(value) self.__jails[name].filter.setFindTime(value)
def getFindTime(self, name): def getFindTime(self, name):
return self.__jails.getFilter(name).getFindTime() return self.__jails[name].filter.getFindTime()
def setDatePattern(self, name, pattern): def setDatePattern(self, name, pattern):
self.__jails.getFilter(name).setDatePattern(pattern) self.__jails[name].filter.setDatePattern(pattern)
def getDatePattern(self, name): def getDatePattern(self, name):
return self.__jails.getFilter(name).getDatePattern() return self.__jails[name].filter.getDatePattern()
def setIgnoreCommand(self, name, value): def setIgnoreCommand(self, name, value):
self.__jails.getFilter(name).setIgnoreCommand(value) self.__jails[name].filter.setIgnoreCommand(value)
def getIgnoreCommand(self, name): def getIgnoreCommand(self, name):
return self.__jails.getFilter(name).getIgnoreCommand() return self.__jails[name].filter.getIgnoreCommand()
def addFailRegex(self, name, value): def addFailRegex(self, name, value):
self.__jails.getFilter(name).addFailRegex(value) self.__jails[name].filter.addFailRegex(value)
def delFailRegex(self, name, index): def delFailRegex(self, name, index):
self.__jails.getFilter(name).delFailRegex(index) self.__jails[name].filter.delFailRegex(index)
def getFailRegex(self, name): def getFailRegex(self, name):
return self.__jails.getFilter(name).getFailRegex() return self.__jails[name].filter.getFailRegex()
def addIgnoreRegex(self, name, value): def addIgnoreRegex(self, name, value):
self.__jails.getFilter(name).addIgnoreRegex(value) self.__jails[name].filter.addIgnoreRegex(value)
def delIgnoreRegex(self, name, index): def delIgnoreRegex(self, name, index):
self.__jails.getFilter(name).delIgnoreRegex(index) self.__jails[name].filter.delIgnoreRegex(index)
def getIgnoreRegex(self, name): def getIgnoreRegex(self, name):
return self.__jails.getFilter(name).getIgnoreRegex() return self.__jails[name].filter.getIgnoreRegex()
def setUseDns(self, name, value): def setUseDns(self, name, value):
self.__jails.getFilter(name).setUseDns(value) self.__jails[name].filter.setUseDns(value)
def getUseDns(self, name): def getUseDns(self, name):
return self.__jails.getFilter(name).getUseDns() return self.__jails[name].filter.getUseDns()
def setMaxRetry(self, name, value): def setMaxRetry(self, name, value):
self.__jails.getFilter(name).setMaxRetry(value) self.__jails[name].filter.setMaxRetry(value)
def getMaxRetry(self, name): def getMaxRetry(self, name):
return self.__jails.getFilter(name).getMaxRetry() return self.__jails[name].filter.getMaxRetry()
def setMaxLines(self, name, value): def setMaxLines(self, name, value):
self.__jails.getFilter(name).setMaxLines(value) self.__jails[name].filter.setMaxLines(value)
def getMaxLines(self, name): def getMaxLines(self, name):
return self.__jails.getFilter(name).getMaxLines() return self.__jails[name].filter.getMaxLines()
# Action # Action
def addAction(self, name, value): def addAction(self, name, value, *args):
self.__jails.getAction(name).addAction(value) self.__jails[name].actions.add(value, *args)
def getLastAction(self, name):
return self.__jails.getAction(name).getLastAction()
def getActions(self, name): def getActions(self, name):
return self.__jails.getAction(name).getActions() return self.__jails[name].actions
def delAction(self, name, value): def delAction(self, name, value):
self.__jails.getAction(name).delAction(value) del self.__jails[name].actions[value]
def setCInfo(self, name, action, key, value): def getAction(self, name, value):
self.__jails.getAction(name).getAction(action).setCInfo(key, value) return self.__jails[name].actions[value]
def getCInfo(self, name, action, key):
return self.__jails.getAction(name).getAction(action).getCInfo(key)
def delCInfo(self, name, action, key):
self.__jails.getAction(name).getAction(action).delCInfo(key)
def setBanTime(self, name, value): def setBanTime(self, name, value):
self.__jails.getAction(name).setBanTime(value) self.__jails[name].actions.setBanTime(value)
def setBanIP(self, name, value): def setBanIP(self, name, value):
return self.__jails.getFilter(name).addBannedIP(value) return self.__jails[name].filter.addBannedIP(value)
def setUnbanIP(self, name, value): def setUnbanIP(self, name, value):
return self.__jails.getAction(name).removeBannedIP(value) self.__jails[name].actions.removeBannedIP(value)
def getBanTime(self, name): def getBanTime(self, name):
return self.__jails.getAction(name).getBanTime() return self.__jails[name].actions.getBanTime()
def setActionStart(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionStart(value)
def getActionStart(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionStart()
def setActionStop(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionStop(value)
def getActionStop(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionStop()
def setActionCheck(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionCheck(value)
def getActionCheck(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionCheck()
def setActionBan(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionBan(value)
def getActionBan(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionBan()
def setActionUnban(self, name, action, value):
self.__jails.getAction(name).getAction(action).setActionUnban(value)
def getActionUnban(self, name, action):
return self.__jails.getAction(name).getAction(action).getActionUnban()
def setActionTimeout(self, name, action, value):
self.__jails.getAction(name).getAction(action).setTimeout(value)
def getActionTimeout(self, name, action):
return self.__jails.getAction(name).getAction(action).getTimeout()
# Status # Status
def status(self): def status(self):
try: try:
self.__lock.acquire() self.__lock.acquire()
jails = list(self.__jails.getAll()) jails = list(self.__jails)
jails.sort() jails.sort()
jailList = ", ".join(jails) jailList = ", ".join(jails)
ret = [("Number of jail", self.__jails.size()), ret = [("Number of jail", len(self.__jails)),
("Jail list", jailList)] ("Jail list", jailList)]
return ret return ret
finally: finally:
self.__lock.release() self.__lock.release()
def statusJail(self, name): def statusJail(self, name):
return self.__jails.get(name).getStatus() return self.__jails[name].getStatus()
# Logging # Logging
@ -488,7 +444,7 @@ class Server:
return "flushed" return "flushed"
def setDatabase(self, filename): def setDatabase(self, filename):
if self.__jails.size() == 0: if len(self.__jails) == 0:
if filename.lower() == "none": if filename.lower() == "none":
self.__db = None self.__db = None
else: else:

View File

@ -25,6 +25,7 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import logging, time import logging, time
import json
# Gets the instance of the logger. # Gets the instance of the logger.
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -226,56 +227,29 @@ class Transmitter:
return self.__server.setBanIP(name,value) return self.__server.setBanIP(name,value)
elif command[1] == "unbanip": elif command[1] == "unbanip":
value = command[2] value = command[2]
return self.__server.setUnbanIP(name,value) self.__server.setUnbanIP(name, value)
return value
elif command[1] == "addaction": elif command[1] == "addaction":
value = command[2] args = [command[2]]
self.__server.addAction(name, value) if len(command) > 3:
return self.__server.getLastAction(name).getName() args.extend([command[3], json.loads(command[4])])
self.__server.addAction(name, *args)
return args[0]
elif command[1] == "delaction": elif command[1] == "delaction":
value = command[2] value = command[2]
self.__server.delAction(name, value) self.__server.delAction(name, value)
return None return None
elif command[1] == "setcinfo": elif command[1] == "action":
act = command[2] actionname = command[2]
key = command[3] actionkey = command[3]
value = " ".join(command[4:]) action = self.__server.getAction(name, actionname)
self.__server.setCInfo(name, act, key, value) if callable(getattr(action, actionkey, None)):
return self.__server.getCInfo(name, act, key) actionvalue = json.loads(command[4]) if len(command)>4 else {}
elif command[1] == "delcinfo": return getattr(action, actionkey)(**actionvalue)
act = command[2] else:
key = command[3] actionvalue = command[4]
self.__server.delCInfo(name, act, key) setattr(action, actionkey, actionvalue)
return None return getattr(action, actionkey)
elif command[1] == "actionstart":
act = command[2]
value = " ".join(command[3:])
self.__server.setActionStart(name, act, value)
return self.__server.getActionStart(name, act)
elif command[1] == "actionstop":
act = command[2]
value = " ".join(command[3:])
self.__server.setActionStop(name, act, value)
return self.__server.getActionStop(name, act)
elif command[1] == "actioncheck":
act = command[2]
value = " ".join(command[3:])
self.__server.setActionCheck(name, act, value)
return self.__server.getActionCheck(name, act)
elif command[1] == "actionban":
act = command[2]
value = " ".join(command[3:])
self.__server.setActionBan(name, act, value)
return self.__server.getActionBan(name, act)
elif command[1] == "actionunban":
act = command[2]
value = " ".join(command[3:])
self.__server.setActionUnban(name, act, value)
return self.__server.getActionUnban(name, act)
elif command[1] == "timeout":
act = command[2]
value = int(command[3])
self.__server.setActionTimeout(name, act, value)
return self.__server.getActionTimeout(name, act)
raise Exception("Invalid command (no set action or not yet implemented)") raise Exception("Invalid command (no set action or not yet implemented)")
def __commandGet(self, command): def __commandGet(self, command):
@ -327,31 +301,25 @@ class Transmitter:
elif command[1] == "bantime": elif command[1] == "bantime":
return self.__server.getBanTime(name) return self.__server.getBanTime(name)
elif command[1] == "actions": elif command[1] == "actions":
return self.__server.getActions(name) return self.__server.getActions(name).keys()
elif command[1] == "addaction": elif command[1] == "action":
return self.__server.getLastAction(name).getName() actionname = command[2]
elif command[1] == "actionstart": actionvalue = command[3]
act = command[2] action = self.__server.getAction(name, actionname)
return self.__server.getActionStart(name, act) return getattr(action, actionvalue)
elif command[1] == "actionstop": elif command[1] == "actionproperties":
act = command[2] actionname = command[2]
return self.__server.getActionStop(name, act) action = self.__server.getAction(name, actionname)
elif command[1] == "actioncheck": return [
act = command[2] key for key in dir(action)
return self.__server.getActionCheck(name, act) if not key.startswith("_") and
elif command[1] == "actionban": not callable(getattr(action, key))]
act = command[2] elif command[1] == "actionmethods":
return self.__server.getActionBan(name, act) actionname = command[2]
elif command[1] == "actionunban": action = self.__server.getAction(name, actionname)
act = command[2] return [
return self.__server.getActionUnban(name, act) key for key in dir(action)
elif command[1] == "cinfo": if not key.startswith("_") and callable(getattr(action, key))]
act = command[2]
key = command[3]
return self.__server.getCInfo(name, act, key)
elif command[1] == "timeout":
act = command[2]
return self.__server.getActionTimeout(name, act)
raise Exception("Invalid command (no get action or not yet implemented)") raise Exception("Invalid command (no get action or not yet implemented)")
def status(self, command): def status(self, command):

View File

@ -26,39 +26,49 @@ __license__ = "GPL"
import unittest, time import unittest, time
import sys, os, tempfile import sys, os, tempfile
from fail2ban.server.actions import Actions
from dummyjail import DummyJail
class ExecuteActions(unittest.TestCase): from ..server.actions import Actions
from .dummyjail import DummyJail
from .utils import LogCaptureTestCase
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
class ExecuteActions(LogCaptureTestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
super(ExecuteActions, self).setUp()
self.__jail = DummyJail() self.__jail = DummyJail()
self.__actions = Actions(self.__jail) self.__actions = Actions(self.__jail)
self.__tmpfile, self.__tmpfilename = tempfile.mkstemp() self.__tmpfile, self.__tmpfilename = tempfile.mkstemp()
def tearDown(self): def tearDown(self):
super(ExecuteActions, self).tearDown()
os.remove(self.__tmpfilename) os.remove(self.__tmpfilename)
def defaultActions(self): def defaultActions(self):
self.__actions.addAction('ip') self.__actions.add('ip')
self.__ip = self.__actions.getAction('ip') self.__ip = self.__actions['ip']
self.__ip.setActionStart('echo ip start 64 >> "%s"' % self.__tmpfilename ) self.__ip.actionstart = 'echo ip start 64 >> "%s"' % self.__tmpfilename
self.__ip.setActionBan('echo ip ban <ip> >> "%s"' % self.__tmpfilename ) self.__ip.actionban = 'echo ip ban <ip> >> "%s"' % self.__tmpfilename
self.__ip.setActionUnban('echo ip unban <ip> >> "%s"' % self.__tmpfilename ) self.__ip.actionunban = 'echo ip unban <ip> >> "%s"' % self.__tmpfilename
self.__ip.setActionCheck('echo ip check <ip> >> "%s"' % self.__tmpfilename ) self.__ip.actioncheck = 'echo ip check <ip> >> "%s"' % self.__tmpfilename
self.__ip.setActionStop('echo ip stop >> "%s"' % self.__tmpfilename ) self.__ip.actionstop = 'echo ip stop >> "%s"' % self.__tmpfilename
def testActionsAddDuplicateName(self):
self.__actions.add('test')
self.assertRaises(ValueError, self.__actions.add, 'test')
def testActionsManipulation(self): def testActionsManipulation(self):
self.__actions.addAction('test') self.__actions.add('test')
self.assertTrue(self.__actions.getAction('test')) self.assertTrue(self.__actions['test'])
self.assertTrue(self.__actions.getLastAction()) self.assertTrue('test' in self.__actions)
self.assertRaises(KeyError,self.__actions.getAction,*['nonexistant action']) self.assertFalse('nonexistant action' in self.__actions)
self.__actions.addAction('test1') self.__actions.add('test1')
self.__actions.delAction('test') del self.__actions['test']
self.__actions.delAction('test1') del self.__actions['test1']
self.assertRaises(KeyError, self.__actions.getAction, *['test']) self.assertFalse('test' in self.__actions)
self.assertRaises(IndexError,self.__actions.getLastAction) self.assertEqual(len(self.__actions), 0)
self.__actions.setBanTime(127) self.__actions.setBanTime(127)
self.assertEqual(self.__actions.getBanTime(),127) self.assertEqual(self.__actions.getBanTime(),127)
@ -77,3 +87,55 @@ class ExecuteActions(unittest.TestCase):
self.assertEqual(self.__actions.status(),[("Currently banned", 0 ), self.assertEqual(self.__actions.status(),[("Currently banned", 0 ),
("Total banned", 0 ), ("IP list", [] )]) ("Total banned", 0 ), ("IP list", [] )])
def testAddActionPython(self):
self.__actions.add(
"Action", os.path.join(TEST_FILES_DIR, "action.d/action.py"),
{'opt1': 'value'})
self.assertTrue(self._is_logged("TestAction initialised"))
self.__actions.start()
time.sleep(3)
self.assertTrue(self._is_logged("TestAction action start"))
self.__actions.stop()
self.__actions.join()
self.assertTrue(self._is_logged("TestAction action stop"))
self.assertRaises(IOError,
self.__actions.add, "Action3", "/does/not/exist.py", {})
# With optional argument
self.__actions.add(
"Action4", os.path.join(TEST_FILES_DIR, "action.d/action.py"),
{'opt1': 'value', 'opt2': 'value2'})
# With too many arguments
self.assertRaises(
TypeError, self.__actions.add, "Action5",
os.path.join(TEST_FILES_DIR, "action.d/action.py"),
{'opt1': 'value', 'opt2': 'value2', 'opt3': 'value3'})
# Missing required argument
self.assertRaises(
TypeError, self.__actions.add, "Action5",
os.path.join(TEST_FILES_DIR, "action.d/action.py"), {})
def testAddPythonActionNOK(self):
self.assertRaises(RuntimeError, self.__actions.add,
"Action", os.path.join(TEST_FILES_DIR,
"action.d/action_noAction.py"),
{})
self.assertRaises(RuntimeError, self.__actions.add,
"Action", os.path.join(TEST_FILES_DIR,
"action.d/action_nomethod.py"),
{})
self.__actions.add(
"Action", os.path.join(TEST_FILES_DIR,
"action.d/action_errors.py"),
{})
self.__actions.start()
time.sleep(3)
self.assertTrue(self._is_logged("Failed to start"))
self.__actions.stop()
self.__actions.join()
self.assertTrue(self._is_logged("Failed to stop"))

View File

@ -27,26 +27,21 @@ __license__ = "GPL"
import time import time
import logging, sys import logging, sys
from fail2ban.server.action import Action from ..server.action import CommandAction, CallingMap
from fail2ban.tests.utils import LogCaptureTestCase from .utils import LogCaptureTestCase
class ExecuteAction(LogCaptureTestCase): class CommandActionTest(LogCaptureTestCase):
def setUp(self): def setUp(self):
"""Call before every test case.""" """Call before every test case."""
self.__action = Action("Test") self.__action = CommandAction(None, "Test")
LogCaptureTestCase.setUp(self) LogCaptureTestCase.setUp(self)
def tearDown(self): def tearDown(self):
"""Call after every test case.""" """Call after every test case."""
LogCaptureTestCase.tearDown(self) LogCaptureTestCase.tearDown(self)
self.__action.execActionStop() self.__action.stop()
def testNameChange(self):
self.assertEqual(self.__action.getName(), "Test")
self.__action.setName("Tricky Test")
self.assertEqual(self.__action.getName(), "Tricky Test")
def testSubstituteRecursiveTags(self): def testSubstituteRecursiveTags(self):
aInfo = { aInfo = {
@ -55,18 +50,18 @@ class ExecuteAction(LogCaptureTestCase):
'xyz': "890 <ABC>", 'xyz': "890 <ABC>",
} }
# Recursion is bad # Recursion is bad
self.assertFalse(Action.substituteRecursiveTags({'A': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<A>'}))
self.assertFalse(Action.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': '<B>', 'B': '<C>', 'C': '<A>'}))
# part recursion # Unresolveable substition
self.assertFalse(Action.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''})) self.assertFalse(CommandAction.substituteRecursiveTags({'A': 'to=<B> fromip=<IP>', 'C': '<B>', 'B': '<C>', 'D': ''}))
self.assertFalse(Action.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''})) self.assertFalse(CommandAction.substituteRecursiveTags({'failregex': 'to=<honeypot> fromip=<IP>', 'sweet': '<honeypot>', 'honeypot': '<sweet>', 'ignoreregex': ''}))
# missing tags are ok # missing tags are ok
self.assertEqual(Action.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'}) self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C>'}), {'A': '<C>'})
self.assertEqual(Action.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> fun', 'X':'fun'}) self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C> <D> <X>','X':'fun'}), {'A': '<C> <D> fun', 'X':'fun'})
self.assertEqual(Action.substituteRecursiveTags({'A': '<C> <B>', 'B': 'cool'}), {'A': '<C> cool', 'B': 'cool'}) self.assertEqual(CommandAction.substituteRecursiveTags({'A': '<C> <B>', 'B': 'cool'}), {'A': '<C> cool', 'B': 'cool'})
# rest is just cool # rest is just cool
self.assertEqual(Action.substituteRecursiveTags(aInfo), self.assertEqual(CommandAction.substituteRecursiveTags(aInfo),
{ 'HOST': "192.0.2.0", { 'HOST': "192.0.2.0",
'ABC': '123 192.0.2.0', 'ABC': '123 192.0.2.0',
'xyz': '890 123 192.0.2.0', 'xyz': '890 123 192.0.2.0',
@ -102,90 +97,102 @@ class ExecuteAction(LogCaptureTestCase):
# Callable # Callable
self.assertEqual( self.assertEqual(
self.__action.replaceTag("09 <callable> 11", self.__action.replaceTag("09 <callme> 11",
{'callable': lambda: str(10)}), CallingMap(callme=lambda: str(10))),
"09 10 11") "09 10 11")
# As tag not present, therefore callable should not be called # As tag not present, therefore callable should not be called
# Will raise ValueError if it is # Will raise ValueError if it is
self.assertEqual( self.assertEqual(
self.__action.replaceTag("abc", self.__action.replaceTag("abc",
{'callable': lambda: int("a")}), "abc") CallingMap(callme=lambda: int("a"))), "abc")
def testExecuteActionBan(self): def testExecuteActionBan(self):
self.__action.setActionStart("touch /tmp/fail2ban.test") self.__action.actionstart = "touch /tmp/fail2ban.test"
self.assertEqual(self.__action.getActionStart(), "touch /tmp/fail2ban.test") self.assertEqual(self.__action.actionstart, "touch /tmp/fail2ban.test")
self.__action.setActionStop("rm -f /tmp/fail2ban.test") self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.assertEqual(self.__action.getActionStop(), 'rm -f /tmp/fail2ban.test') self.assertEqual(self.__action.actionstop, 'rm -f /tmp/fail2ban.test')
self.__action.setActionBan("echo -n") self.__action.actionban = "echo -n"
self.assertEqual(self.__action.getActionBan(), 'echo -n') self.assertEqual(self.__action.actionban, 'echo -n')
self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]") self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.assertEqual(self.__action.getActionCheck(), '[ -e /tmp/fail2ban.test ]') self.assertEqual(self.__action.actioncheck, '[ -e /tmp/fail2ban.test ]')
self.__action.setActionUnban("true") self.__action.actionunban = "true"
self.assertEqual(self.__action.getActionUnban(), 'true') self.assertEqual(self.__action.actionunban, 'true')
self.assertFalse(self._is_logged('returned')) self.assertFalse(self._is_logged('returned'))
# no action was actually executed yet # no action was actually executed yet
self.assertTrue(self.__action.execActionBan(None)) self.__action.ban({'ip': None})
self.assertTrue(self._is_logged('Invariant check failed')) self.assertTrue(self._is_logged('Invariant check failed'))
self.assertTrue(self._is_logged('returned successfully')) self.assertTrue(self._is_logged('returned successfully'))
def testExecuteActionEmptyUnban(self): def testExecuteActionEmptyUnban(self):
self.__action.setActionUnban("") self.__action.actionunban = ""
self.assertTrue(self.__action.execActionUnban(None)) self.__action.unban({})
self.assertTrue(self._is_logged('Nothing to do')) self.assertTrue(self._is_logged('Nothing to do'))
def testExecuteActionStartCtags(self): def testExecuteActionStartCtags(self):
self.__action.setCInfo("HOST","192.0.2.0") self.__action.HOST = "192.0.2.0"
self.__action.setActionStart("touch /tmp/fail2ban.test.<HOST>") self.__action.actionstart = "touch /tmp/fail2ban.test.<HOST>"
self.__action.setActionStop("rm -f /tmp/fail2ban.test.<HOST>") self.__action.actionstop = "rm -f /tmp/fail2ban.test.<HOST>"
self.__action.setActionCheck("[ -e /tmp/fail2ban.test.192.0.2.0 ]") self.__action.actioncheck = "[ -e /tmp/fail2ban.test.192.0.2.0 ]"
self.assertTrue(self.__action.execActionStart()) self.__action.start()
def testExecuteActionCheckRestoreEnvironment(self): def testExecuteActionCheckRestoreEnvironment(self):
self.__action.setActionStart("") self.__action.actionstart = ""
self.__action.setActionStop("rm -f /tmp/fail2ban.test") self.__action.actionstop = "rm -f /tmp/fail2ban.test"
self.__action.setActionBan("rm /tmp/fail2ban.test") self.__action.actionban = "rm /tmp/fail2ban.test"
self.__action.setActionCheck("[ -e /tmp/fail2ban.test ]") self.__action.actioncheck = "[ -e /tmp/fail2ban.test ]"
self.assertFalse(self.__action.execActionBan(None)) self.assertRaises(RuntimeError, self.__action.ban, {'ip': None})
self.assertTrue(self._is_logged('Unable to restore environment')) self.assertTrue(self._is_logged('Unable to restore environment'))
def testExecuteActionChangeCtags(self): def testExecuteActionChangeCtags(self):
self.__action.setCInfo("ROST","192.0.2.0") self.assertRaises(AttributeError, getattr, self.__action, "ROST")
self.assertEqual(self.__action.getCInfo("ROST"),"192.0.2.0") self.__action.ROST = "192.0.2.0"
self.__action.delCInfo("ROST") self.assertEqual(self.__action.ROST,"192.0.2.0")
self.assertRaises(KeyError, self.__action.getCInfo, "ROST")
def testExecuteActionUnbanAinfo(self): def testExecuteActionUnbanAinfo(self):
aInfo = { aInfo = {
'ABC': "123", 'ABC': "123",
} }
self.__action.setActionBan("touch /tmp/fail2ban.test.123") self.__action.actionban = "touch /tmp/fail2ban.test.123"
self.__action.setActionUnban("rm /tmp/fail2ban.test.<ABC>") self.__action.actionunban = "rm /tmp/fail2ban.test.<ABC>"
self.assertTrue(self.__action.execActionBan(None)) self.__action.ban(aInfo)
self.assertTrue(self.__action.execActionUnban(aInfo)) self.__action.unban(aInfo)
def testExecuteActionStartEmpty(self): def testExecuteActionStartEmpty(self):
self.__action.setActionStart("") self.__action.actionstart = ""
self.assertTrue(self.__action.execActionStart()) self.__action.start()
self.assertTrue(self._is_logged('Nothing to do')) self.assertTrue(self._is_logged('Nothing to do'))
def testExecuteIncorrectCmd(self): def testExecuteIncorrectCmd(self):
Action.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null') CommandAction.executeCmd('/bin/ls >/dev/null\nbogusXXX now 2>/dev/null')
self.assertTrue(self._is_logged('HINT on 127: "Command not found"')) self.assertTrue(self._is_logged('HINT on 127: "Command not found"'))
def testExecuteTimeout(self): def testExecuteTimeout(self):
stime = time.time() stime = time.time()
Action.executeCmd('sleep 60', timeout=2) # Should take a minute # Should take a minute
self.assertRaises(
RuntimeError, CommandAction.executeCmd, 'sleep 60', timeout=2)
self.assertAlmostEqual(time.time() - stime, 2, places=0) self.assertAlmostEqual(time.time() - stime, 2, places=0)
self.assertTrue(self._is_logged('sleep 60 -- timed out after 2 seconds')) self.assertTrue(self._is_logged('sleep 60 -- timed out after 2 seconds'))
self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM')) self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM'))
def testCaptureStdOutErr(self): def testCaptureStdOutErr(self):
Action.executeCmd('echo "How now brown cow"') CommandAction.executeCmd('echo "How now brown cow"')
self.assertTrue(self._is_logged("'How now brown cow\\n'")) self.assertTrue(self._is_logged("'How now brown cow\\n'"))
Action.executeCmd( CommandAction.executeCmd(
'echo "The rain in Spain stays mainly in the plain" 1>&2') 'echo "The rain in Spain stays mainly in the plain" 1>&2')
self.assertTrue(self._is_logged( self.assertTrue(self._is_logged(
"'The rain in Spain stays mainly in the plain\\n'")) "'The rain in Spain stays mainly in the plain\\n'"))
def testCallingMap(self):
mymap = CallingMap(callme=lambda: str(10), error=lambda: int('a'),
dontcallme= "string", number=17)
# Should work fine
self.assertEqual(
"%(callme)s okay %(dontcallme)s %(number)i" % mymap,
"10 okay string 17")
# Error will now trip, demonstrating delayed call
self.assertRaises(ValueError, lambda x: "%(error)i" % x, mymap)

View File

@ -26,8 +26,8 @@ __license__ = "GPL"
import unittest import unittest
from fail2ban.server.banmanager import BanManager from ..server.banmanager import BanManager
from fail2ban.server.ticket import BanTicket from ..server.ticket import BanTicket
class AddFailure(unittest.TestCase): class AddFailure(unittest.TestCase):

View File

@ -23,19 +23,22 @@ __license__ = "GPL"
import os, shutil, sys, tempfile, unittest import os, shutil, sys, tempfile, unittest
from fail2ban.client.configreader import ConfigReader from ..client.configreader import ConfigReader
from fail2ban.client.jailreader import JailReader from ..client.jailreader import JailReader
from fail2ban.client.filterreader import FilterReader from ..client.filterreader import FilterReader
from fail2ban.client.jailsreader import JailsReader from ..client.jailsreader import JailsReader
from fail2ban.client.actionreader import ActionReader from ..client.actionreader import ActionReader
from fail2ban.client.configurator import Configurator from ..client.configurator import Configurator
from fail2ban.tests.utils import LogCaptureTestCase from .utils import LogCaptureTestCase
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
if os.path.exists('config/fail2ban.conf'): if os.path.exists(os.path.join('config','fail2ban.conf')):
CONFIG_DIR='config' CONFIG_DIR='config'
else: else:
CONFIG_DIR='/etc/fail2ban' CONFIG_DIR=os.path.join('etc','fail2ban')
IMPERFECT_CONFIG = os.path.join('fail2ban', 'tests','config')
IMPERFECT_CONFIG = os.path.join('fail2ban', 'tests','config') IMPERFECT_CONFIG = os.path.join('fail2ban', 'tests','config')
@ -381,13 +384,18 @@ class JailsReaderTest(LogCaptureTestCase):
['set', 'brokenaction', 'addaction', 'brokenaction'], ['set', 'brokenaction', 'addaction', 'brokenaction'],
['set', ['set',
'brokenaction', 'brokenaction',
'actionban', 'action',
'brokenaction', 'brokenaction',
'actionban',
'hit with big stick <ip>'], 'hit with big stick <ip>'],
['set', 'brokenaction', 'actionstop', 'brokenaction', ''], ['set', 'brokenaction', 'action', 'brokenaction',
['set', 'brokenaction', 'actionstart', 'brokenaction', ''], 'actionstop', ''],
['set', 'brokenaction', 'actionunban', 'brokenaction', ''], ['set', 'brokenaction', 'action', 'brokenaction',
['set', 'brokenaction', 'actioncheck', 'brokenaction', ''], 'actionstart', ''],
['set', 'brokenaction', 'action', 'brokenaction',
'actionunban', ''],
['set', 'brokenaction', 'action', 'brokenaction',
'actioncheck', ''],
['add', 'parse_to_end_of_jail.conf', 'auto'], ['add', 'parse_to_end_of_jail.conf', 'auto'],
['set', 'parse_to_end_of_jail.conf', 'usedns', 'warn'], ['set', 'parse_to_end_of_jail.conf', 'usedns', 'warn'],
['set', 'parse_to_end_of_jail.conf', 'maxretry', 3], ['set', 'parse_to_end_of_jail.conf', 'maxretry', 3],
@ -514,7 +522,7 @@ class JailsReaderTest(LogCaptureTestCase):
self.assertTrue('blocktype' in action._initOpts) self.assertTrue('blocktype' in action._initOpts)
# Verify that we have a call to set it up # Verify that we have a call to set it up
blocktype_present = False blocktype_present = False
target_command = [ 'set', jail_name, 'setcinfo', action_name, 'blocktype' ] target_command = ['set', jail_name, 'action', action_name, 'blocktype']
for command in commands: for command in commands:
if (len(command) > 5 and if (len(command) > 5 and
command[:5] == target_command): command[:5] == target_command):
@ -567,6 +575,8 @@ class JailsReaderTest(LogCaptureTestCase):
[testjail1] [testjail1]
action = testaction1[actname=test1] action = testaction1[actname=test1]
testaction1[actname=test2] testaction1[actname=test2]
testaction.py
testaction.py[actname=test3]
filter = testfilter1 filter = testfilter1
""") """)
jailfd.close() jailfd.close()
@ -575,8 +585,12 @@ filter = testfilter1
self.assertTrue(jails.getOptions()) self.assertTrue(jails.getOptions())
comm_commands = jails.convert(allow_no_files=True) comm_commands = jails.convert(allow_no_files=True)
action_names = [comm[-1] for comm in comm_commands if comm[:3] == ['set', 'testjail1', 'addaction']] add_actions = [comm[3:] for comm in comm_commands
if comm[:3] == ['set', 'testjail1', 'addaction']]
self.assertNotEqual(len(set(action_names)), 1) self.assertEqual(len(set(action[0] for action in add_actions)), 4)
# Python actions should not be passed `actname`
self.assertEqual(add_actions[-1][-1], "{}")
shutil.rmtree(basedir) shutil.rmtree(basedir)

View File

@ -28,11 +28,11 @@ import tempfile
import sqlite3 import sqlite3
import shutil import shutil
from fail2ban.server.database import Fail2BanDb from ..server.database import Fail2BanDb
from fail2ban.server.filter import FileContainer from ..server.filter import FileContainer
from fail2ban.server.mytime import MyTime from ..server.mytime import MyTime
from fail2ban.server.ticket import FailTicket from ..server.ticket import FailTicket
from fail2ban.tests.dummyjail import DummyJail from .dummyjail import DummyJail
class DatabaseTest(unittest.TestCase): class DatabaseTest(unittest.TestCase):

View File

@ -25,10 +25,10 @@ __copyright__ = "Copyright (c) 2004 Cyril Jaquier"
__license__ = "GPL" __license__ = "GPL"
import unittest, calendar, time, datetime, re, pprint import unittest, calendar, time, datetime, re, pprint
from fail2ban.server.datedetector import DateDetector from ..server.datedetector import DateDetector
from fail2ban.server.datetemplate import DateTemplate from ..server.datetemplate import DateTemplate
from fail2ban.server.iso8601 import Utc from ..server.iso8601 import Utc
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime from .utils import setUpMyTime, tearDownMyTime
class DateDetectorTest(unittest.TestCase): class DateDetectorTest(unittest.TestCase):

View File

@ -26,8 +26,8 @@ __license__ = "GPL"
import unittest, socket, time, pickle import unittest, socket, time, pickle
from fail2ban.server.failmanager import FailManager, FailManagerEmpty from ..server.failmanager import FailManager, FailManagerEmpty
from fail2ban.server.ticket import FailTicket from ..server.ticket import FailTicket
class AddFailure(unittest.TestCase): class AddFailure(unittest.TestCase):

View File

@ -0,0 +1,28 @@
from fail2ban.server.action import ActionBase
class TestAction(ActionBase):
def __init__(self, jail, name, opt1, opt2=None):
super(TestAction, self).__init__(jail, name)
self._logSys.debug("%s initialised" % self.__class__.__name__)
self.opt1 = opt1
self.opt2 = opt2
self._opt3 = "Hello"
def start(self):
self._logSys.debug("%s action start" % self.__class__.__name__)
def stop(self):
self._logSys.debug("%s action stop" % self.__class__.__name__)
def ban(self, aInfo):
self._logSys.debug("%s action ban" % self.__class__.__name__)
def unban(self, aInfo):
self._logSys.debug("%s action unban" % self.__class__.__name__)
def testmethod(self, text):
return "%s %s %s" % (self._opt3, text, self.opt1)
Action = TestAction

View File

@ -0,0 +1,21 @@
from fail2ban.server.action import ActionBase
class TestAction(ActionBase):
def __init__(self, jail, name):
super(TestAction, self).__init__(jail, name)
def start(self):
raise Exception()
def stop(self):
raise Exception()
def ban(self):
raise Exception()
def unban(self):
raise Exception()
Action = TestAction

View File

@ -0,0 +1,5 @@
from fail2ban.server.action import ActionBase
class TestAction(ActionBase):
pass

View File

@ -0,0 +1,12 @@
from fail2ban.server.action import ActionBase
class TestAction():
def __init__(self, jail, name):
pass
def start(self):
pass
Action = TestAction

View File

@ -17,4 +17,10 @@
# https://github.com/fail2ban/fail2ban/issues/541 # https://github.com/fail2ban/fail2ban/issues/541
# failJSON: { "time": "2013-12-30T00:24:50", "match": true , "host": "178.123.108.196" } # failJSON: { "time": "2013-12-30T00:24:50", "match": true , "host": "178.123.108.196" }
2013-12-30 00:24:50 1VxPit-000MMd-U4 SA: Action: flagged as Spam but accepted: score=8.2 required=5.0 (scanned in 6/6 secs | Message-Id: 008701cf04ed_24497d70_6cdc7850_@xxx.xx). From <spammer@xxx.xx> (host=ip-4.net-3-2-1.rev.xxx.xx [178.123.108.196]) for trap@example.com 2013-12-30 00:24:50 1VxPit-000MMd-U4 SA: Action: flagged as Spam but accepted: score=8.2 required=5.0 (scanned in 6/6 secs | Message-Id: 008701cf04ed_24497d70_6cdc7850_@xxx.xx). From <spammer@xxx.xx> (host=ip-4.net-3-2-1.rev.xxx.xx [178.123.108.196]) for trap@example.com
# https://github.com/fail2ban/fail2ban/issues/533
# failJSON: { "time": "2013-12-29T15:34:12", "match": true , "host": "188.76.45.72" }
2013-12-29 15:34:12 1VxHRO-000NiI-Ly SA: Action: silently tossed message: score=31.0 required=5.0 trigger=30.0 (scanned in 6/6 secs | Message-Id: etPan.09bd0c40.c3d5f675.fdf7@server.local). From <Flossiedpd@jazztel.es> (host=72.45.76.188.dynamic.jazztel.es [188.76.45.72]) for me@my.com
# https://github.com/fail2ban/fail2ban/issues/533
# failJSON: { "time": "2013-12-29T15:39:11", "match": true , "host": "178.123.108.196" }
2013-12-29 15:39:11 1VxHWD-000NuW-83 SA: Action: silently tossed message: score=35.8 required=5.0 trigger=30.0 (scanned in 6/6 secs | Message-Id: 1VxHWD-000NuW-83). From <> (host=NULL [178.123.108.196]) for me@my.com

View File

@ -0,0 +1,6 @@
# failJSON: { "time": "2004-11-11T18:57:57", "match": true , "host": "203.16.208.190" }
Nov 11 18:57:57 HORDE [error] [horde] FAILED LOGIN for graham [203.16.208.190] to Horde [on line 116 of "/home/ace-hosting/public_html/horde/login.php"]
# failJSON: { "time": "2004-12-15T08:59:59", "match": true , "host": "1.2.3.4" }
Dec 15 08:59:59 HORDE [error] [imp] FAILED LOGIN for emai.user@somedomain.com [1.2.3.4] to {mx.somedomain.com:993 [imap/ssl/novalidate-cert]} [pid 68394 on line 139 of /usr/local/www/www.somedomain.com/public_html/horde/imp/lib/Auth/imp.php"]

View File

@ -0,0 +1,6 @@
# failJSON: { "time": "2013-12-28T19:03:53", "match": true , "host": "178.123.108.196" }
Sat Dec 28 19:03:53 2013 - [72926] (178.123.108.196) gsdfg - userinfo error - auth_unix.pl, ret -4, User gsdfg doesn't exist
# failJSON: { "time": "2013-12-28T19:04:03", "match": true , "host": "178.123.108.196" }
Sat Dec 28 19:04:03 2013 - [72926] (178.123.108.196) gsdfg - login error - no such user - loginname=gsdfg
# failJSON: { "time": "2013-12-28T19:05:38", "match": true , "host": "178.123.108.196" }
Sat Dec 28 19:05:38 2013 - [73540] (178.123.108.196) myname - login error - auth_unix.pl, ret -4, Password incorrect

View File

@ -0,0 +1,2 @@
# failJSON: { "time": "2011-11-21T14:29:16", "match": true, "host": "10.7.41.61" }
2011.11.21 14:29:16 LOG3[28228:140093368055552]: SSL_accept from 10.7.41.61:33454 : 140890C7: error:140890C7:SSL routines:SSL3_GET_CLIENT_CERTIFICATE:peer did not return a certificate

View File

@ -34,19 +34,16 @@ try:
except ImportError: except ImportError:
journal = None journal = None
from fail2ban.server.jail import Jail from ..server.jail import Jail
from fail2ban.server.filterpoll import FilterPoll from ..server.filterpoll import FilterPoll
from fail2ban.server.filter import Filter, FileFilter, DNSUtils from ..server.filter import Filter, FileFilter, DNSUtils
from fail2ban.server.failmanager import FailManager from ..server.failmanager import FailManager, FailManagerEmpty
from fail2ban.server.failmanager import FailManagerEmpty from ..server.mytime import MyTime
from fail2ban.server.mytime import MyTime from .utils import setUpMyTime, tearDownMyTime, mtimesleep, LogCaptureTestCase
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime from .dummyjail import DummyJail
from fail2ban.tests.utils import mtimesleep, LogCaptureTestCase
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
from fail2ban.tests.dummyjail import DummyJail
# yoh: per Steven Hiscocks's insight while troubleshooting # yoh: per Steven Hiscocks's insight while troubleshooting
# https://github.com/fail2ban/fail2ban/issues/103#issuecomment-15542836 # https://github.com/fail2ban/fail2ban/issues/103#issuecomment-15542836
# adding a sufficiently large buffer might help to guarantee that # adding a sufficiently large buffer might help to guarantee that

View File

@ -28,8 +28,8 @@ import shutil
from glob import glob from glob import glob
from utils import mbasename, TraceBack, FormatterWithTraceBack from .utils import mbasename, TraceBack, FormatterWithTraceBack
from fail2ban.helpers import formatExceptionInfo from ..helpers import formatExceptionInfo
class HelpersTest(unittest.TestCase): class HelpersTest(unittest.TestCase):

View File

@ -30,9 +30,9 @@ else:
import simplejson as json import simplejson as json
next = lambda x: x.next() next = lambda x: x.next()
from fail2ban.server.filter import Filter from ..server.filter import Filter
from fail2ban.client.filterreader import FilterReader from ..client.filterreader import FilterReader
from fail2ban.tests.utils import setUpMyTime, tearDownMyTime from .utils import setUpMyTime, tearDownMyTime
TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files") TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "files")
if os.path.exists('config/fail2ban.conf'): if os.path.exists('config/fail2ban.conf'):
@ -67,6 +67,7 @@ def testSampleRegexsFactory(name):
# Check filter exists # Check filter exists
filterConf = FilterReader(name, "jail", {}, basedir=CONFIG_DIR) filterConf = FilterReader(name, "jail", {}, basedir=CONFIG_DIR)
self.assertEqual(filterConf.getFile(), name) self.assertEqual(filterConf.getFile(), name)
self.assertEqual(filterConf.getJailName(), "jail")
filterConf.read() filterConf.read()
filterConf.getOptions({}) filterConf.getOptions({})

View File

@ -26,11 +26,11 @@ __license__ = "GPL"
import unittest, socket, time, tempfile, os, locale, sys, logging import unittest, socket, time, tempfile, os, locale, sys, logging
from fail2ban.server.failregex import Regex, FailRegex, RegexException from ..server.failregex import Regex, FailRegex, RegexException
from fail2ban.server.server import Server, logSys from ..server.server import Server, logSys
from fail2ban.server.jail import Jail from ..server.jail import Jail
from fail2ban.exceptions import UnknownJailException from ..exceptions import UnknownJailException
from fail2ban.tests.utils import LogCaptureTestCase from .utils import LogCaptureTestCase
#from bin.fail2ban-client import Fail2banClient #from bin.fail2ban-client import Fail2banClient
try: try:
from fail2ban.server import filtersystemd from fail2ban.server import filtersystemd
@ -518,45 +518,38 @@ class Transmitter(TransmitterBase):
self.assertEqual( self.assertEqual(
self.transm.proceed(["set", self.jailName, "addaction", action]), self.transm.proceed(["set", self.jailName, "addaction", action]),
(0, action)) (0, action))
self.assertEqual(
self.transm.proceed(["get", self.jailName, "addaction"]),
(0, action))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["get", self.jailName, "actions"])[1][0].getName(), ["get", self.jailName, "actions"])[1][0],
action) action)
for cmd, value in zip(cmdList, cmdValueList): for cmd, value in zip(cmdList, cmdValueList):
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, cmd, action, value]), ["set", self.jailName, "action", action, cmd, value]),
(0, value)) (0, value))
for cmd, value in zip(cmdList, cmdValueList): for cmd, value in zip(cmdList, cmdValueList):
self.assertEqual( self.assertEqual(
self.transm.proceed(["get", self.jailName, cmd, action]), self.transm.proceed(["get", self.jailName, "action", action, cmd]),
(0, value)) (0, value))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "setcinfo", action, "KEY", "VALUE"]), ["set", self.jailName, "action", action, "KEY", "VALUE"]),
(0, "VALUE")) (0, "VALUE"))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["get", self.jailName, "cinfo", action, "KEY"]), ["get", self.jailName, "action", action, "KEY"]),
(0, "VALUE")) (0, "VALUE"))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["get", self.jailName, "cinfo", action, "InvalidKey"])[0], ["get", self.jailName, "action", action, "InvalidKey"])[0],
1) 1)
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "delcinfo", action, "KEY"]), ["set", self.jailName, "action", action, "timeout", "10"]),
(0, None))
self.assertEqual(
self.transm.proceed(
["set", self.jailName, "timeout", action, "10"]),
(0, 10)) (0, 10))
self.assertEqual( self.assertEqual(
self.transm.proceed( self.transm.proceed(
["get", self.jailName, "timeout", action]), ["get", self.jailName, "action", action, "timeout"]),
(0, 10)) (0, 10))
self.assertEqual( self.assertEqual(
self.transm.proceed(["set", self.jailName, "delaction", action]), self.transm.proceed(["set", self.jailName, "delaction", action]),
@ -565,6 +558,42 @@ class Transmitter(TransmitterBase):
self.transm.proceed( self.transm.proceed(
["set", self.jailName, "delaction", "Doesn't exist"])[0],1) ["set", self.jailName, "delaction", "Doesn't exist"])[0],1)
def testPythonActionMethodsAndProperties(self):
action = "TestCaseAction"
self.assertEqual(
self.transm.proceed(["set", self.jailName, "addaction", action,
os.path.join(TEST_FILES_DIR, "action.d", "action.py"),
'{"opt1": "value"}']),
(0, action))
self.assertEqual(
sorted(self.transm.proceed(["get", self.jailName,
"actionproperties", action])[1]),
['opt1', 'opt2'])
self.assertEqual(
self.transm.proceed(["get", self.jailName, "action", action,
"opt1"]),
(0, 'value'))
self.assertEqual(
self.transm.proceed(["get", self.jailName, "action", action,
"opt2"]),
(0, None))
self.assertEqual(
sorted(self.transm.proceed(["get", self.jailName, "actionmethods",
action])[1]),
['ban', 'start', 'stop', 'testmethod', 'unban'])
self.assertEqual(
self.transm.proceed(["set", self.jailName, "action", action,
"testmethod", '{"text": "world!"}']),
(0, 'Hello world! value'))
self.assertEqual(
self.transm.proceed(["set", self.jailName, "action", action,
"opt1", "another value"]),
(0, 'another value'))
self.assertEqual(
self.transm.proceed(["set", self.jailName, "action", action,
"testmethod", '{"text": "world!"}']),
(0, 'Hello world! another value'))
def testNOK(self): def testNOK(self):
self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1) self.assertEqual(self.transm.proceed(["INVALID", "COMMAND"])[0],1)

View File

@ -26,8 +26,8 @@ __license__ = "GPL"
import unittest, time, tempfile, os, threading import unittest, time, tempfile, os, threading
from fail2ban.server.asyncserver import AsyncServer, AsyncServerException from ..server.asyncserver import AsyncServer, AsyncServerException
from fail2ban.client.csocket import CSocket from ..client.csocket import CSocket
class Socket(unittest.TestCase): class Socket(unittest.TestCase):

View File

@ -25,16 +25,9 @@ __license__ = "GPL"
import logging, os, re, traceback, time, unittest, sys import logging, os, re, traceback, time, unittest, sys
from os.path import basename, dirname from os.path import basename, dirname
from StringIO import StringIO from StringIO import StringIO
import json
if sys.version_info >= (2, 6): from ..server.mytime import MyTime
import json
else:
try:
import simplejson as json
except ImportError:
json = None
from fail2ban.server.mytime import MyTime
logSys = logging.getLogger(__name__) logSys = logging.getLogger(__name__)
@ -138,19 +131,18 @@ def tearDownMyTime():
def gatherTests(regexps=None, no_network=False): def gatherTests(regexps=None, no_network=False):
# Import all the test cases here instead of a module level to # Import all the test cases here instead of a module level to
# avoid circular imports # avoid circular imports
from fail2ban.tests import banmanagertestcase from . import banmanagertestcase
from fail2ban.tests import clientreadertestcase from . import clientreadertestcase
from fail2ban.tests import failmanagertestcase from . import failmanagertestcase
from fail2ban.tests import filtertestcase from . import filtertestcase
from fail2ban.tests import servertestcase from . import servertestcase
from fail2ban.tests import datedetectortestcase from . import datedetectortestcase
from fail2ban.tests import actiontestcase from . import actiontestcase
from fail2ban.tests import actionstestcase from . import actionstestcase
from fail2ban.tests import sockettestcase from . import sockettestcase
from fail2ban.tests import misctestcase from . import misctestcase
from fail2ban.tests import databasetestcase from . import databasetestcase
if json: from . import samplestestcase
from fail2ban.tests import samplestestcase
if not regexps: # pragma: no cover if not regexps: # pragma: no cover
tests = unittest.TestSuite() tests = unittest.TestSuite()
@ -172,7 +164,7 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite(servertestcase.Transmitter)) tests.addTest(unittest.makeSuite(servertestcase.Transmitter))
tests.addTest(unittest.makeSuite(servertestcase.JailTests)) tests.addTest(unittest.makeSuite(servertestcase.JailTests))
tests.addTest(unittest.makeSuite(servertestcase.RegexTests)) tests.addTest(unittest.makeSuite(servertestcase.RegexTests))
tests.addTest(unittest.makeSuite(actiontestcase.ExecuteAction)) tests.addTest(unittest.makeSuite(actiontestcase.CommandActionTest))
tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions)) tests.addTest(unittest.makeSuite(actionstestcase.ExecuteActions))
# FailManager # FailManager
tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure)) tests.addTest(unittest.makeSuite(failmanagertestcase.AddFailure))
@ -207,30 +199,27 @@ def gatherTests(regexps=None, no_network=False):
# DateDetector # DateDetector
tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest)) tests.addTest(unittest.makeSuite(datedetectortestcase.DateDetectorTest))
if json:
# Filter Regex tests with sample logs # Filter Regex tests with sample logs
tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex)) tests.addTest(unittest.makeSuite(samplestestcase.FilterSamplesRegex))
else:
logSys.warning("I: Skipping filter samples testing. No simplejson/json module")
# #
# Extensive use-tests of different available filters backends # Extensive use-tests of different available filters backends
# #
from fail2ban.server.filterpoll import FilterPoll from ..server.filterpoll import FilterPoll
filters = [FilterPoll] # always available filters = [FilterPoll] # always available
# Additional filters available only if external modules are available # Additional filters available only if external modules are available
# yoh: Since I do not know better way for parametric tests # yoh: Since I do not know better way for parametric tests
# with good old unittest # with good old unittest
try: try:
from fail2ban.server.filtergamin import FilterGamin from ..server.filtergamin import FilterGamin
filters.append(FilterGamin) filters.append(FilterGamin)
except Exception, e: # pragma: no cover except Exception, e: # pragma: no cover
logSys.warning("Skipping gamin backend testing. Got exception '%s'" % e) logSys.warning("Skipping gamin backend testing. Got exception '%s'" % e)
try: try:
from fail2ban.server.filterpyinotify import FilterPyinotify from ..server.filterpyinotify import FilterPyinotify
filters.append(FilterPyinotify) filters.append(FilterPyinotify)
except Exception, e: # pragma: no cover except Exception, e: # pragma: no cover
logSys.warning("I: Skipping pyinotify backend testing. Got exception '%s'" % e) logSys.warning("I: Skipping pyinotify backend testing. Got exception '%s'" % e)
@ -239,7 +228,7 @@ def gatherTests(regexps=None, no_network=False):
tests.addTest(unittest.makeSuite( tests.addTest(unittest.makeSuite(
filtertestcase.get_monitor_failures_testcase(Filter_))) filtertestcase.get_monitor_failures_testcase(Filter_)))
try: # pragma: systemd no cover try: # pragma: systemd no cover
from fail2ban.server.filtersystemd import FilterSystemd from ..server.filtersystemd import FilterSystemd
tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd))) tests.addTest(unittest.makeSuite(filtertestcase.get_monitor_failures_journal_testcase(FilterSystemd)))
except Exception, e: # pragma: no cover except Exception, e: # pragma: no cover
logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e) logSys.warning("I: Skipping systemd backend testing. Got exception '%s'" % e)

View File

@ -7,7 +7,7 @@ jail.conf \- configuration for the fail2ban server
.I jail.conf / jail.local .I jail.conf / jail.local
.I action.d/*.conf action.d/*.local .I action.d/*.conf action.d/*.local action.d/*.py
.I filter.d/*.conf filter.d/*.local .I filter.d/*.conf filter.d/*.local
.SH DESCRIPTION .SH DESCRIPTION
@ -113,13 +113,15 @@ uses systemd python library to access the systemd journal. Specifying \fBlogpath
will try to use the following backends, in order: pyinotify, gamin, polling will try to use the following backends, in order: pyinotify, gamin, polling
.PP .PP
.SS Actions .SS Actions
Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename. In the case where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplicatione.g.: Each jail can be configured with only a single filter, but may have multiple actions. By default, the name of a action is the action filename, and in the case of Python actions, the ".py" file extension is stripped. Where multiple of the same action are to be used, the \fBactname\fR option can be assigned to the action to avoid duplication e.g.:
.PP .PP
.nf .nf
[ssh-iptables-ipset] [ssh-iptables-ipset]
enabled = true enabled = true
action = sendmail[name=ssh, dest=john@example.com, actname=mail-john] action = sendmail[name=ssh, dest=john@example.com, actname=mail-john]
sendmail[name=ssh, dest=paul@example.com, actname=mail-paul] sendmail[name=ssh, dest=paul@example.com, actname=mail-paul]
smtp.py[dest=chris@example.com, actname=smtp-chris]
smtp.py[dest=sally@example.com, actname=smtp-sally]
.fi .fi
.SH "ACTION FILES" .SH "ACTION FILES"
@ -130,8 +132,6 @@ and override the required settings.
Action files are ini files that have two sections, \fBDefinition\fR and \fBInit\fR . Action files are ini files that have two sections, \fBDefinition\fR and \fBInit\fR .
The [Init] section allows for action-specific settings. In \fIjail.conf/jail.local\fR these can be overwritten for a particular jail as options to the jail.
The following commands can be present in the [Definition] section. The following commands can be present in the [Definition] section.
.TP .TP
\fBactionstart\fR \fBactionstart\fR
@ -148,12 +148,16 @@ command(s) that bans the IP address after \fBmaxretry\fR log lines matches withi
.TP .TP
\fBactionunban\fR \fBactionunban\fR
command(s) that unbans the IP address after \fBbantime\fR. command(s) that unbans the IP address after \fBbantime\fR.
.PP
The [Init] section allows for action-specific settings. In \fIjail.conf/jail.local\fR these can be overwritten for a particular jail as options to the jail. The following are special tags which can be set in the [Init] section:
.TP
\fBtimeout\fR
The maximum period of time in seconds that a command can executed, before being killed.
.PP
Commands specified in the [Definition] section are executed through a system shell so shell redirection and process control is allowed. The commands should Commands specified in the [Definition] section are executed through a system shell so shell redirection and process control is allowed. The commands should
return 0, otherwise error would be logged. Moreover if \fBactioncheck\fR exits with non-0 status, it is taken as indication that firewall status has changed and fail2ban needs to reinitialize itself (i.e. issue \fBactionstop\fR and \fBactionstart\fR commands). return 0, otherwise error would be logged. Moreover if \fBactioncheck\fR exits with non-0 status, it is taken as indication that firewall status has changed and fail2ban needs to reinitialize itself (i.e. issue \fBactionstop\fR and \fBactionstart\fR commands).
Tags are enclosed in <>. All the elements of [Init] are tags that are replaced in all action commands. Tags can be added by the Tags are enclosed in <>. All the elements of [Init] are tags that are replaced in all action commands. Tags can be added by the
\fBfail2ban-client\fR using the setctag command. \fB<br>\fR is a tag that is always a new line (\\n). \fBfail2ban-client\fR using the "set <JAIL> action <ACT>" command. \fB<br>\fR is a tag that is always a new line (\\n).
More than a single command is allowed to be specified. Each command needs to be on a separate line and indented with whitespaces without blank lines. The following example defines More than a single command is allowed to be specified. Each command needs to be on a separate line and indented with whitespaces without blank lines. The following example defines
two commands to be executed. two commands to be executed.
@ -188,6 +192,8 @@ As per \fBmatches\fR, but includes all lines for the IP which are contained with
\fBipjailmatches\fR \fBipjailmatches\fR
As per \fBipmatches\fR, but matches are limited for the IP and for the current jail. As per \fBipmatches\fR, but matches are limited for the IP and for the current jail.
.SH "PYTHON ACTION FILES"
Python based actions can also be used, where the file name must be \fI[actionname].py\fR. The Python file must contain a variable \fIAction\fR which points to Python class. This class must implement a minimum interface as described by \fIfail2ban.server.action.ActionBase\fR, which can be inherited from to ease implementation.
.SH FILTER FILES .SH FILTER FILES
Filter definitions are those in \fI/etc/fail2ban/filter.d/*.conf\fR and \fIfilter.d/*.local\fR. Filter definitions are those in \fI/etc/fail2ban/filter.d/*.conf\fR and \fIfilter.d/*.local\fR.

View File

@ -120,7 +120,8 @@ setup(
glob("config/filter.d/*.conf") glob("config/filter.d/*.conf")
), ),
('/etc/fail2ban/action.d', ('/etc/fail2ban/action.d',
glob("config/action.d/*.conf") glob("config/action.d/*.conf") +
glob("config/action.d/*.py")
), ),
('/etc/fail2ban/fail2ban.d', ('/etc/fail2ban/fail2ban.d',
'' ''